feat: RSS embeddings, semantic news in chat, article-to-chat, richer briefings

- Embed RSS items at fetch time (nomic-embed-text); backfill at startup
- Semantic news search injected into chat system prompt ("Recent News You've Seen")
  when items match query above 0.55 cosine threshold (independent of note RAG)
- "Discuss in chat" button on news cards — creates a seeded conversation with
  the article title + full content, navigates directly to the new chat
- Briefing compilation now passes 500-char article excerpts (not just headlines)
  to the LLM and uses 8192 num_ctx to accommodate the larger prompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 15:12:38 -04:00
parent dba41879ed
commit a773c11aa0
11 changed files with 327 additions and 8 deletions
@@ -233,7 +233,7 @@ async def _gather_external(user_id: int) -> dict:
# ── LLM synthesis ─────────────────────────────────────────────────────────────
async def _llm_synthesise(system_prompt: str, user_prompt: str, model: str) -> str:
async def _llm_synthesise(system_prompt: str, user_prompt: str, model: str, num_ctx: int = 4096) -> str:
"""Single non-streaming LLM call. Returns the assistant's response text."""
payload = {
"model": model,
@@ -242,7 +242,7 @@ async def _llm_synthesise(system_prompt: str, user_prompt: str, model: str) -> s
{"role": "user", "content": user_prompt},
],
"stream": False,
"options": {"num_ctx": 4096, "temperature": 0.4},
"options": {"num_ctx": num_ctx, "temperature": 0.4},
}
try:
async with httpx.AsyncClient(timeout=120.0) as client:
@@ -309,13 +309,17 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te
lines.append(f" (and {len(overdue) - 3} more)")
lines.append("")
# News highlights (top 3 — right panel shows full list)
# News highlights (top 3 with excerpts — right panel shows full list)
rss = external_data.get("rss_items") or []
if rss:
lines.append("NEWS HIGHLIGHTS (mention 1-2 briefly, the full list is shown separately):")
lines.append("NEWS HIGHLIGHTS (weave 1-2 into your briefing naturally; the full list is shown separately):")
for item in rss[:3]:
source = item.get("feed_title") or item.get("source") or "News"
lines.append(f" [{source}] {item.get('title', '')}")
title = item.get("title", "")
excerpt = (item.get("content") or item.get("snippet") or "")[:500].strip()
lines.append(f" [{source}] {title}")
if excerpt:
lines.append(f" {excerpt}")
lines.append("")
return "\n".join(lines)
@@ -427,6 +431,7 @@ async def run_compilation(
_unified_system_prompt(profile_context),
_unified_user_prompt(internal_data_filtered, external_data_filtered, slot, temp_unit),
model,
num_ctx=8192,
)
# ── Post-processing ─────────────────────────────────────────────────────────