Files
pablan/backend/app/authoring/context.py
T
ProfessorNovaandClaude Opus 5 97dbff309c Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 08:36:17 +02:00

68 lines
2.3 KiB
Python

"""Conversation context for a capture.
When a document is written out of a chat, that chat's subject is useful twice:
to find existing documents the user might extend, and as background for section
refinement. Both use a short LLM topic summary of the conversation. All LLM
traffic goes through `llm/client.py`; nothing here logs content — a failure
degrades quietly rather than breaking the capture.
"""
import logging
from pydantic import BaseModel
from app.authoring.prompts import render_topic_summary_prompt
from app.llm.client import chat_json
from app.llm.errors import LLMError
from app.models import Conversation, MessageRole
logger = logging.getLogger("pablan.authoring")
# How many recent turns feed the summary — enough for the subject, bounded so
# a long thread cannot blow up the utility prompt.
MAX_CONTEXT_MESSAGES = 12
# Skip the reasoning model's hidden thinking: this is a short, latency-
# sensitive utility call.
_NO_THINKING = {"chat_template_kwargs": {"enable_thinking": False}}
class _TopicSummary(BaseModel):
topic: str
def conversation_transcript(conversation: Conversation) -> str:
"""The recent user/assistant turns as a plain transcript."""
turns = [
message
for message in conversation.messages
if message.role in (MessageRole.user, MessageRole.assistant)
][-MAX_CONTEXT_MESSAGES:]
return "\n".join(
f"{'User' if message.role == MessageRole.user else 'Assistant'}: "
f"{message.content}"
for message in turns
)
async def summarize_transcript(transcript: str) -> str:
"""A short topic summary of a conversation transcript, or '' if none can
be made. Failures degrade quietly."""
if not transcript.strip():
return ""
try:
result = await chat_json(
render_topic_summary_prompt(transcript),
_TopicSummary,
extra_body=_NO_THINKING,
)
except LLMError:
logger.info("topic summary failed", extra={"event": "topic_summary_failed"})
return ""
return result.topic.strip()
async def summarize_conversation(conversation: Conversation) -> str:
"""A short topic summary of the conversation, or '' if none can be made."""
return await summarize_transcript(conversation_transcript(conversation))