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:
@@ -16,7 +16,7 @@ from fabledassistant.config import Config
|
||||
from fabledassistant.models import async_session
|
||||
from fabledassistant.models.conversation import Message
|
||||
from fabledassistant.services.generation_buffer import GenerationBuffer, GenerationState
|
||||
from fabledassistant.services.llm import ChatChunk, build_context, generate_completion, stream_chat, stream_chat_with_tools
|
||||
from fabledassistant.services.llm import ChatChunk, build_context, generate_completion, stream_chat, stream_chat_with_tools, summarize_history_for_context
|
||||
from fabledassistant.services.chat import update_conversation_title
|
||||
from fabledassistant.services.intent import IntentResult, classify_intent
|
||||
from fabledassistant.services.logging import log_generation
|
||||
@@ -172,43 +172,55 @@ async def run_generation(
|
||||
MAX_TOOL_ROUNDS = 5
|
||||
msg_id = buf.assistant_message_id
|
||||
|
||||
# Phase 1: launch all independent work in parallel so nothing waits on anything
|
||||
# unnecessarily. build_context (note search + system prompt) and the intent LLM
|
||||
# call are the two slow legs — run them concurrently.
|
||||
buf.append_event("status", {"status": "Building context..."})
|
||||
|
||||
context_task = asyncio.create_task(build_context(
|
||||
user_id, history, context_note_id, user_content, exclude_note_ids=exclude_note_ids
|
||||
))
|
||||
tools_task = asyncio.create_task(get_tools_for_user(user_id))
|
||||
intent_model_task = asyncio.create_task(get_setting(user_id, "intent_model", ""))
|
||||
|
||||
# Tools + intent-model setting are fast DB calls — get them first so intent
|
||||
# can start immediately while build_context is still running.
|
||||
tools, intent_model_setting = await asyncio.gather(tools_task, intent_model_task)
|
||||
# Phase 1: Quick DB calls — resolve tools list and intent model in parallel.
|
||||
tools, intent_model_setting = await asyncio.gather(
|
||||
get_tools_for_user(user_id),
|
||||
get_setting(user_id, "intent_model", ""),
|
||||
)
|
||||
intent_model = intent_model_setting or Config.OLLAMA_INTENT_MODEL or model
|
||||
logger.info(
|
||||
"Starting generation for conv %d: model=%s, intent_model=%s, tools=%d",
|
||||
conv_id, model, intent_model, len(tools),
|
||||
)
|
||||
|
||||
# Start intent classification in parallel with remaining build_context work.
|
||||
# Phase 2: Summarize long conversation history if needed.
|
||||
# For short conversations (<= threshold) this returns immediately with no LLM call.
|
||||
# For long conversations it spends ~500ms to compress old exchanges, which
|
||||
# more than pays for itself by reducing the prefill token count every turn.
|
||||
history_to_use = history
|
||||
history_summary: str | None = None
|
||||
if len(history) > 20: # matches _HISTORY_SUMMARY_THRESHOLD in llm.py
|
||||
buf.append_event("status", {"status": "Summarizing conversation history..."})
|
||||
history_to_use, history_summary = await summarize_history_for_context(history, intent_model)
|
||||
|
||||
# Phase 3: Build context and classify intent in parallel — the two slow legs.
|
||||
pre_intent: IntentResult = IntentResult()
|
||||
intent_timing_ms: int | None = None
|
||||
if tools:
|
||||
intent_history = [
|
||||
m for m in history
|
||||
m for m in history_to_use
|
||||
if m.get("role") in ("user", "assistant") and m.get("content")
|
||||
][-6:]
|
||||
buf.append_event("status", {"status": "Analyzing your request..."})
|
||||
t_intent = time.monotonic()
|
||||
context_task = asyncio.create_task(build_context(
|
||||
user_id, history_to_use, context_note_id, user_content,
|
||||
exclude_note_ids=exclude_note_ids,
|
||||
history_summary=history_summary,
|
||||
))
|
||||
intent_task = asyncio.create_task(
|
||||
classify_intent(user_content, tools, intent_model, history=intent_history)
|
||||
)
|
||||
(messages, context_meta), pre_intent = await asyncio.gather(context_task, intent_task)
|
||||
intent_timing_ms = int((time.monotonic() - t_intent) * 1000)
|
||||
else:
|
||||
messages, context_meta = await context_task
|
||||
messages, context_meta = await build_context(
|
||||
user_id, history_to_use, context_note_id, user_content,
|
||||
exclude_note_ids=exclude_note_ids,
|
||||
history_summary=history_summary,
|
||||
)
|
||||
|
||||
# Emit context event
|
||||
buf.append_event("context", {"context": context_meta})
|
||||
|
||||
Reference in New Issue
Block a user