Ctrl K
ComunidadGuías…

rex/conversation_memory.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Lightweight conversation memory with heuristic summarisation."""

from __future__ import annotations

from collections import deque
from dataclasses import dataclass


@dataclass
class MemoryTurn:
    """Represents a single utterance in the conversation."""

    role: str
    content: str


class ConversationMemory:
    """Maintain recent conversation turns with compact summaries."""

    def __init__(
        self,
        *,
        max_turns: int = 6,
        summary_trigger: int = 4,
    ) -> None:
        if max_turns < 1:
            raise ValueError("max_turns must be at least 1")
        if summary_trigger < 1:
            raise ValueError("summary_trigger must be at least 1")
        self.max_turns = max_turns
        self.summary_trigger = summary_trigger
        self._turns: deque[MemoryTurn] = deque()
        self._summary: str = ""

    # ------------------------------------------------------------------
    # Mutation helpers
    # ------------------------------------------------------------------
    def add_turn(self, user_text: str, assistant_text: str) -> None:
        """Record a user/assistant exchange and update summaries."""

        user_clean = (user_text or "").strip()
        assistant_clean = (assistant_text or "").strip()

        if user_clean:
            self._turns.append(MemoryTurn("user", user_clean))
        if assistant_clean:
            self._turns.append(MemoryTurn("assistant", assistant_clean))

        if len(self._turns) // 2 > self.max_turns:
            self._summarise_history()

    def reset(self) -> None:
        """Clear stored history and summaries."""

        self._turns.clear()
        self._summary = ""

    # ------------------------------------------------------------------
    # Accessors
    # ------------------------------------------------------------------
    @property
    def summary(self) -> str:
        return self._summary

    @property
    def turns(self) -> list[MemoryTurn]:
        return list(self._turns)

    # ------------------------------------------------------------------
    # Prompt construction
    # ------------------------------------------------------------------
    def build_messages(
        self,
        *,
        persona: str,
        profile_context: str,
        user_text: str,
    ) -> list[dict[str, str]]:
        """Return structured chat messages for the active interaction."""

        if not user_text or not user_text.strip():
            raise ValueError("user_text must not be empty")

        messages: list[dict[str, str]] = []
        if persona.strip():
            messages.append({"role": "system", "content": persona.strip()})
        if self._summary:
            messages.append({"role": "system", "content": self._summary})
        if profile_context.strip():
            messages.append({"role": "system", "content": profile_context.strip()})
        for turn in self._turns:
            messages.append({"role": turn.role, "content": turn.content})
        messages.append({"role": "user", "content": user_text.strip()})
        return messages

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------
    def _summarise_history(self) -> None:
        keep_turns = max(1, self.summary_trigger) * 2
        if len(self._turns) <= keep_turns:
            return

        stale = list(self._turns)[:-keep_turns]
        if not stale:
            return

        summary_lines: list[str] = []
        i = 0
        while i < len(stale):
            user = stale[i].content if i < len(stale) else ""
            assistant = ""
            if i + 1 < len(stale) and stale[i + 1].role == "assistant":
                assistant = stale[i + 1].content
                i += 2
            else:
                i += 1
            if not user and not assistant:
                continue
            snippet_user = (user[:80] + "…") if len(user) > 80 else user
            snippet_assistant = (
                (assistant[:80] + "…") if len(assistant) > 80 else assistant or "(no reply)"
            )
            summary_lines.append(
                f"- User asked about '{snippet_user}' and the assistant replied '{snippet_assistant}'."
            )

        if summary_lines:
            summary_text = "Previous exchanges:\n" + "\n".join(summary_lines)
            if self._summary:
                self._summary = f"{self._summary}\n{summary_text}"
            else:
                self._summary = summary_text

        self._turns = deque(list(self._turns)[-keep_turns:])


__all__ = ["ConversationMemory", "MemoryTurn"]