Generate conversation titles via LLM instead of truncating first line

- _maybe_set_title sets a quick truncated fallback immediately
- Then fires a background thread to ask the LLM for a 5-8 word summary
- Background thread doesn't block the streaming response
- Title appears in sidebar on first chunk, then upgrades when LLM responds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cora-start
PeninsulaInd 2026-02-23 11:11:34 -06:00
parent d771dd5c80
commit 30757b5bcf
1 changed files with 43 additions and 6 deletions

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import base64 import base64
import json import json
import logging import logging
import threading
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path from pathlib import Path
@ -294,19 +295,55 @@ class Agent:
self._memory.auto_flush(conv_id) self._memory.auto_flush(conv_id)
def _maybe_set_title(self, conv_id: str, user_input: str): def _maybe_set_title(self, conv_id: str, user_input: str):
"""Set conversation title from first user message if still 'New Chat'.""" """Set conversation title from first user message if still 'New Chat'.
Sets a quick truncated fallback immediately, then fires a background
thread to generate a proper 5-8 word LLM summary.
"""
try: try:
current_title = self.db.get_conversation_title(conv_id) current_title = self.db.get_conversation_title(conv_id)
if current_title and current_title != "New Chat": if current_title and current_title != "New Chat":
return return
title = user_input.split("\n", 1)[0].strip() # Immediate fallback: first line, truncated
if len(title) > 50: fallback = user_input.split("\n", 1)[0].strip()
title = title[:47] + "..." if len(fallback) > 50:
if title: fallback = fallback[:47] + "..."
self.db.update_conversation_title(conv_id, title) if fallback:
self.db.update_conversation_title(conv_id, fallback)
# Fire background LLM call to generate a better title
threading.Thread(
target=self._generate_title,
args=(conv_id, user_input),
daemon=True,
).start()
except Exception as e: except Exception as e:
log.warning("Failed to set conversation title: %s", e) log.warning("Failed to set conversation title: %s", e)
def _generate_title(self, conv_id: str, user_input: str):
"""Background: ask the LLM for a 5-8 word conversation title."""
try:
prompt = user_input[:500] # cap input to keep it cheap
messages = [
{
"role": "system",
"content": (
"Generate a short 5-8 word title summarizing this conversation opener. "
"Reply with ONLY the title — no quotes, no punctuation at the end, no explanation."
),
},
{"role": "user", "content": prompt},
]
parts = []
for chunk in self.llm.chat(messages, tools=None, stream=False):
if chunk.get("type") == "text":
parts.append(chunk["content"])
title = "".join(parts).strip().strip('"').strip("'")
if title and len(title) <= 60:
self.db.update_conversation_title(conv_id, title)
log.debug("Generated conversation title: %s", title)
except Exception as e:
log.warning("Background title generation failed: %s", e)
def respond_to_prompt(self, prompt: str) -> str: def respond_to_prompt(self, prompt: str) -> str:
"""Non-streaming response for scheduled tasks / internal use.""" """Non-streaming response for scheduled tasks / internal use."""
result_parts = [] result_parts = []