feat: inject briefing article content for deep article Q&A
Add _build_briefing_article_context() helper to llm.py that reads rss_item_ids from briefing message metadata and injects article content into the system prompt. Pass conv_id through build_context() and generation_task.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,7 @@ async def run_generation(
|
||||
rag_project_id=rag_project_id,
|
||||
workspace_project_id=workspace_project_id,
|
||||
user_timezone=user_timezone,
|
||||
conv_id=conv_id,
|
||||
))
|
||||
|
||||
messages, context_meta = await context_task
|
||||
|
||||
@@ -453,6 +453,7 @@ async def build_context(
|
||||
rag_project_id: int | None = None,
|
||||
workspace_project_id: int | None = None,
|
||||
user_timezone: str | None = None,
|
||||
conv_id: int | None = None,
|
||||
) -> tuple[list[dict], dict]:
|
||||
"""Build messages array for Ollama with system prompt and context.
|
||||
|
||||
@@ -676,7 +677,88 @@ async def build_context(
|
||||
f"\n\n--- Earlier Conversation ---\n{history_summary}\n--- End Earlier Conversation ---"
|
||||
)
|
||||
|
||||
# Inject briefing article content for follow-up Q&A
|
||||
if conv_id is not None:
|
||||
from fabledassistant.models import async_session as _async_session
|
||||
from fabledassistant.models.conversation import Conversation
|
||||
|
||||
async with _async_session() as _sess:
|
||||
_conv = await _sess.get(Conversation, conv_id)
|
||||
if _conv and getattr(_conv, "conversation_type", None) == "briefing":
|
||||
article_context = await _build_briefing_article_context(conv_id)
|
||||
if article_context:
|
||||
system_parts.append(article_context)
|
||||
|
||||
messages = [{"role": "system", "content": "".join(system_parts)}]
|
||||
messages.extend(history)
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
return messages, context_meta
|
||||
|
||||
|
||||
async def _build_briefing_article_context(conv_id: int) -> str:
|
||||
"""Fetch article content from today's briefing message and return a
|
||||
formatted context block for injection into the system prompt.
|
||||
|
||||
Looks at the most recent assistant briefing messages for rss_item_ids
|
||||
in their metadata, then loads those items from the DB.
|
||||
Capped at 10 articles × 500 chars to keep token use reasonable.
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
from sqlalchemy import select, text as _text
|
||||
|
||||
from fabledassistant.models import async_session as _async_session
|
||||
from fabledassistant.models.conversation import Message
|
||||
|
||||
async with _async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Message)
|
||||
.where(
|
||||
Message.conversation_id == conv_id,
|
||||
Message.role == "assistant",
|
||||
)
|
||||
.order_by(Message.created_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
messages = result.scalars().all()
|
||||
|
||||
rss_item_ids: list[int] = []
|
||||
for msg in messages:
|
||||
meta = msg.msg_metadata or {}
|
||||
if isinstance(meta, str):
|
||||
try:
|
||||
meta = _json.loads(meta)
|
||||
except Exception:
|
||||
continue
|
||||
ids = meta.get("rss_item_ids") or []
|
||||
if ids:
|
||||
rss_item_ids = ids
|
||||
break
|
||||
|
||||
if not rss_item_ids:
|
||||
return ""
|
||||
|
||||
async with _async_session() as session:
|
||||
result = await session.execute(
|
||||
_text("""
|
||||
SELECT i.title, i.url, i.content, f.title AS feed_title
|
||||
FROM rss_items i
|
||||
JOIN rss_feeds f ON f.id = i.feed_id
|
||||
WHERE i.id = ANY(:ids)
|
||||
ORDER BY i.published_at DESC NULLS LAST
|
||||
LIMIT 10
|
||||
""").bindparams(ids=rss_item_ids[:10])
|
||||
)
|
||||
rows = result.mappings().all()
|
||||
|
||||
if not rows:
|
||||
return ""
|
||||
|
||||
lines = ["\n\nARTICLE CONTEXT (source articles from today's briefing):"]
|
||||
for row in rows:
|
||||
lines.append(f"\n[{row['feed_title']}] {row['title']}")
|
||||
if row["url"]:
|
||||
lines.append(f"URL: {row['url']}")
|
||||
if row["content"]:
|
||||
lines.append(row["content"][:500])
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -56,6 +56,28 @@ def test_list_news_item_serialisation():
|
||||
assert result["published_at"] == pub.isoformat()
|
||||
|
||||
|
||||
|
||||
def test_build_briefing_article_context_metadata_extraction():
|
||||
"""Verify the rss_item_ids extraction logic from message metadata."""
|
||||
import json
|
||||
|
||||
# Simulates the logic in _build_briefing_article_context
|
||||
def extract_ids(msg_metadata):
|
||||
meta = msg_metadata or {}
|
||||
if isinstance(meta, str):
|
||||
try:
|
||||
meta = json.loads(meta)
|
||||
except Exception:
|
||||
return []
|
||||
return meta.get("rss_item_ids") or []
|
||||
|
||||
assert extract_ids({}) == []
|
||||
assert extract_ids(None) == []
|
||||
assert extract_ids({"rss_item_ids": [1, 2, 3]}) == [1, 2, 3]
|
||||
assert extract_ids(json.dumps({"rss_item_ids": [4, 5]})) == [4, 5]
|
||||
assert extract_ids("not json") == []
|
||||
|
||||
|
||||
def test_list_news_null_content_serialisation():
|
||||
"""Null content should produce empty snippet without error."""
|
||||
item = {
|
||||
|
||||
Reference in New Issue
Block a user