From 058bd76719372410df7b03425de458888b4b303e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 22:53:23 -0400 Subject: [PATCH] feat(briefing): multi-article topic discussion endpoint for deep thematic analysis --- src/fabledassistant/routes/briefing.py | 103 +++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index 3ebca44..d3bbfd5 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -544,3 +544,106 @@ async def discuss_article(item_id: int): )) return jsonify({"assistant_message_id": assistant_msg.id, "status": "generating"}), 202 + + +@briefing_bp.route("/topics//discuss", methods=["POST"]) +@_REQUIRE +async def discuss_topic(topic: str): + """Discuss all recent articles in a topic cluster — multi-article deep analysis.""" + data = await request.get_json() or {} + conv_id = data.get("conv_id") + if not conv_id: + return jsonify({"error": "conv_id required"}), 400 + + uid = g.user.id + + # Verify conversation belongs to user + conv = await get_conversation(uid, conv_id) + if conv is None: + return jsonify({"error": "Conversation not found"}), 404 + + if get_buffer(conv_id) is not None: + return jsonify({"error": "Generation already in progress"}), 409 + + # Find recent articles with this topic (last 2 days) + async with async_session() as session: + result = await session.execute( + select(RssItem) + .join(RssFeed, RssFeed.id == RssItem.feed_id) + .where(RssFeed.user_id == uid) + .where(RssItem.topics.contains([topic])) + .order_by(RssItem.published_at.desc().nullslast()) + .limit(5) + ) + items = list(result.scalars().all()) + + if not items: + return jsonify({"error": f"No articles found for topic '{topic}'"}), 404 + + # Fetch full content for each article + from fabledassistant.services.rss import _fetch_full_article + + synthetic_tool_calls = [] + for item in items: + content = await _fetch_full_article(item.url) if item.url else None + content = content or item.content or "" + synthetic_tool_calls.append({ + "function": "read_article", + "arguments": {"url": item.url or ""}, + "result": { + "success": True, + "type": "article_content", + "url": item.url or "", + "title": item.title or "", + "source": "", + "content": content[:8000], # cap per article to stay within context + "truncated": len(content) > 8000, + }, + }) + + await add_message(conv_id, "assistant", "", status="complete", tool_calls=synthetic_tool_calls) + + user_prompt = ( + f"I'd like to discuss the {len(items)} articles about '{topic}'. " + "Don't just summarize each one — draw connections between the sources, " + "highlight where they agree or disagree, and share your analysis of what " + "this means. Let's have a real discussion about this topic." + ) + await add_message(conv_id, "user", user_prompt) + + conv = await get_conversation(uid, conv_id) + assert conv is not None + + history = [] + for msg in conv.messages: + if msg.role == "system": + continue + msg_dict = {"role": msg.role, "content": msg.content or ""} + if msg.tool_calls: + msg_dict["tool_calls"] = [ + {"function": {"name": tc["function"], "arguments": tc["arguments"]}} + for tc in msg.tool_calls + ] + history.append(msg_dict) + for tc in msg.tool_calls: + history.append({"role": "tool", "content": json.dumps(tc.get("result", {}))}) + else: + history.append(msg_dict) + + model = await get_setting(uid, "default_model", "") or "" + + assistant_msg = await add_message(conv_id, "assistant", "", status="generating") + buf = create_buffer(conv_id, assistant_msg.id) + + asyncio.create_task(run_generation( + buf, history, model, + uid, conv_id, conv.title or "", + user_prompt, + think=True, + )) + + return jsonify({ + "assistant_message_id": assistant_msg.id, + "status": "generating", + "article_count": len(items), + }), 202