Add conversation history summarization for long chats
When a conversation exceeds 20 messages (10 exchanges), the oldest messages are summarized into a compact 3-5 sentence paragraph using the intent model, and only the most recent 6 messages are passed verbatim. The summary is injected into the system prompt so the model retains context without the full token cost. For short conversations the check is O(1) and returns immediately. The status indicator shows "Summarizing conversation history..." when the LLM call is needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -238,12 +238,75 @@ def _find_urls(text: str) -> list[str]:
|
||||
return re.findall(r"https?://[^\s<>\"')\]]+", text)
|
||||
|
||||
|
||||
# History summarization thresholds
|
||||
_HISTORY_SUMMARY_THRESHOLD = 20 # total messages before summarizing
|
||||
_HISTORY_KEEP_RECENT = 6 # verbatim tail to preserve (3 exchanges)
|
||||
|
||||
|
||||
async def summarize_history_for_context(
|
||||
history: list[dict],
|
||||
model: str,
|
||||
) -> tuple[list[dict], str | None]:
|
||||
"""Summarize old conversation history when it exceeds the threshold.
|
||||
|
||||
Returns (recent_history, summary_text | None).
|
||||
recent_history is the verbatim tail passed to the model.
|
||||
summary_text (when not None) should be injected into the system prompt
|
||||
so the model retains the gist of earlier exchanges without the full tokens.
|
||||
For short conversations, returns (history, None) immediately with no LLM call.
|
||||
"""
|
||||
if len(history) <= _HISTORY_SUMMARY_THRESHOLD:
|
||||
return history, None
|
||||
|
||||
to_summarize = history[:-_HISTORY_KEEP_RECENT]
|
||||
recent = history[-_HISTORY_KEEP_RECENT:]
|
||||
|
||||
lines: list[str] = []
|
||||
for m in to_summarize:
|
||||
role = m.get("role", "")
|
||||
content = (m.get("content") or "").strip()
|
||||
if role in ("user", "assistant") and content:
|
||||
label = "User" if role == "user" else "Assistant"
|
||||
lines.append(f"{label}: {content[:400]}")
|
||||
|
||||
if not lines:
|
||||
return history, None
|
||||
|
||||
prompt_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Summarize this conversation history in 3-5 concise sentences. "
|
||||
"Capture: topics discussed, any notes/tasks/events created or modified, "
|
||||
"decisions made, and context needed to continue the conversation naturally. "
|
||||
"Be specific and factual. Output only the summary, nothing else."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": "\n".join(lines)},
|
||||
]
|
||||
|
||||
try:
|
||||
summary = await generate_completion(prompt_messages, model, max_tokens=200)
|
||||
summary = summary.strip()
|
||||
if summary:
|
||||
logger.info(
|
||||
"Summarized %d history messages (%d chars) for context",
|
||||
len(to_summarize), len(summary),
|
||||
)
|
||||
return recent, summary
|
||||
except Exception:
|
||||
logger.warning("Failed to summarize conversation history", exc_info=True)
|
||||
|
||||
return history, None
|
||||
|
||||
|
||||
async def build_context(
|
||||
user_id: int,
|
||||
history: list[dict],
|
||||
current_note_id: int | None,
|
||||
user_message: str,
|
||||
exclude_note_ids: list[int] | None = None,
|
||||
history_summary: str | None = None,
|
||||
) -> tuple[list[dict], dict]:
|
||||
"""Build messages array for Ollama with system prompt and context.
|
||||
|
||||
@@ -340,6 +403,12 @@ async def build_context(
|
||||
f"\n\n--- Content from {url} ---\n{content}\n--- End URL Content ---"
|
||||
)
|
||||
|
||||
# Inject compressed summary of older exchanges when history has been trimmed
|
||||
if history_summary:
|
||||
system_parts.append(
|
||||
f"\n\n--- Earlier Conversation ---\n{history_summary}\n--- End Earlier Conversation ---"
|
||||
)
|
||||
|
||||
messages = [{"role": "system", "content": "".join(system_parts)}]
|
||||
messages.extend(history)
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
Reference in New Issue
Block a user