116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
from datetime import datetime, timedelta
|
|
|
|
from pydantic_ai import Agent
|
|
|
|
from haiku.rag.agents.chat.prompts import SESSION_SUMMARY_PROMPT
|
|
from haiku.rag.agents.chat.state import ChatSessionState, QAResponse, SessionContext
|
|
from haiku.rag.config.models import AppConfig
|
|
from haiku.rag.utils import get_model
|
|
|
|
# Cache for session contexts (session_id -> SessionContext)
|
|
# Used to persist async summarization results between requests
|
|
_session_context_cache: dict[str, SessionContext] = {}
|
|
_cache_timestamps: dict[str, datetime] = {}
|
|
_CACHE_TTL = timedelta(hours=1)
|
|
|
|
|
|
def _cleanup_stale_cache() -> None:
|
|
"""Remove cache entries older than TTL."""
|
|
now = datetime.now()
|
|
stale = [sid for sid, ts in _cache_timestamps.items() if now - ts > _CACHE_TTL]
|
|
for sid in stale:
|
|
_session_context_cache.pop(sid, None)
|
|
_cache_timestamps.pop(sid, None)
|
|
|
|
|
|
def cache_session_context(session_id: str, context: SessionContext) -> None:
|
|
"""Store session context in cache."""
|
|
_cleanup_stale_cache()
|
|
_session_context_cache[session_id] = context
|
|
_cache_timestamps[session_id] = datetime.now()
|
|
|
|
|
|
def get_cached_session_context(session_id: str) -> SessionContext | None:
|
|
"""Get session context from server cache."""
|
|
_cleanup_stale_cache()
|
|
return _session_context_cache.get(session_id)
|
|
|
|
|
|
async def summarize_session(
|
|
qa_history: list[QAResponse],
|
|
config: AppConfig,
|
|
current_context: str | None = None,
|
|
) -> str:
|
|
"""Summarize qa_history into compact context.
|
|
|
|
Args:
|
|
qa_history: List of Q&A pairs from the conversation.
|
|
config: AppConfig for model selection.
|
|
current_context: Previous session_context.summary to incorporate.
|
|
The summarizer will build upon this.
|
|
|
|
Returns:
|
|
Markdown summary of the conversation history.
|
|
"""
|
|
if not qa_history:
|
|
return ""
|
|
|
|
model = get_model(config.qa.model, config)
|
|
agent: Agent[None, str] = Agent(
|
|
model,
|
|
output_type=str,
|
|
instructions=SESSION_SUMMARY_PROMPT,
|
|
retries=2,
|
|
)
|
|
|
|
history_text = _format_qa_history(qa_history)
|
|
if current_context:
|
|
history_text = f"## Current Context\n{current_context}\n\n{history_text}"
|
|
result = await agent.run(history_text)
|
|
return result.output
|
|
|
|
|
|
async def update_session_context(
|
|
qa_history: list[QAResponse],
|
|
config: AppConfig,
|
|
session_state: ChatSessionState,
|
|
) -> None:
|
|
"""Update session context in the session state.
|
|
|
|
Args:
|
|
qa_history: List of Q&A pairs from the conversation.
|
|
config: AppConfig for model selection.
|
|
session_state: The session state to update.
|
|
"""
|
|
# Use existing session_context summary if available, else initial_context
|
|
current_context: str | None = None
|
|
if session_state.session_context and session_state.session_context.summary:
|
|
current_context = session_state.session_context.summary
|
|
elif session_state.initial_context:
|
|
current_context = session_state.initial_context
|
|
|
|
summary = await summarize_session(
|
|
qa_history, config, current_context=current_context
|
|
)
|
|
session_state.session_context = SessionContext(
|
|
summary=summary,
|
|
last_updated=datetime.now(),
|
|
)
|
|
# Also cache for next-run delivery in stateless contexts
|
|
if session_state.session_id:
|
|
cache_session_context(session_state.session_id, session_state.session_context)
|
|
|
|
|
|
def _format_qa_history(qa_history: list[QAResponse]) -> str:
|
|
"""Format qa_history for input to summarization."""
|
|
lines: list[str] = []
|
|
for i, qa in enumerate(qa_history, 1):
|
|
lines.append(f"## Q{i}: {qa.question}")
|
|
lines.append(f"**Answer** (confidence: {qa.confidence:.0%}):")
|
|
lines.append(qa.answer)
|
|
|
|
if qa.sources:
|
|
lines.append(f"**Sources:** {', '.join(qa.sources)}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|