feat(briefing): add article Discuss endpoint with synthetic tool exchange
POST /api/briefing/articles/<id>/discuss injects stored article content as a persisted read_article tool exchange before triggering generation. The LLM sees the article as already read; follow-ups retain context via the fixed history builder. Frontend Discuss button now calls the new endpoint instead of inlining article text in the user message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import WeatherCard from '@/components/WeatherCard.vue'
|
|||||||
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
||||||
import {
|
import {
|
||||||
apiGet,
|
apiGet,
|
||||||
|
apiPost,
|
||||||
getBriefingConfig,
|
getBriefingConfig,
|
||||||
getBriefingConversations,
|
getBriefingConversations,
|
||||||
getBriefingToday,
|
getBriefingToday,
|
||||||
@@ -113,26 +114,19 @@ watch(selectedConvId, async (id) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Keep send() as a helper for discussArticle
|
|
||||||
async function send(text: string) {
|
|
||||||
if (!text || !todayConvId.value || chatStore.streaming) return
|
|
||||||
if (chatStore.currentConversation?.id !== todayConvId.value) {
|
|
||||||
await chatStore.fetchConversation(todayConvId.value)
|
|
||||||
}
|
|
||||||
await chatStore.sendMessage(text, undefined, undefined, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function discussArticle(item: NewsItem) {
|
async function discussArticle(item: NewsItem) {
|
||||||
if (!todayConvId.value) return
|
if (!todayConvId.value || chatStore.streaming) return
|
||||||
if (!isToday.value) selectedConvId.value = todayConvId.value
|
if (!isToday.value) selectedConvId.value = todayConvId.value
|
||||||
const body = (item.content || item.snippet || '').trim()
|
|
||||||
const bodyBlock = body ? `\n\n---\n${body}\n---` : ''
|
|
||||||
const text = `Please summarize and share your thoughts on this article. Do not use any research or search tools — respond conversationally based only on the content below.\n\n**${item.title}**\nSource: ${item.source}${bodyBlock}`
|
|
||||||
// Scroll to chat column
|
|
||||||
await nextTick(() => {
|
await nextTick(() => {
|
||||||
document.querySelector('.briefing-center')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
document.querySelector('.briefing-center')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
})
|
})
|
||||||
await send(text)
|
try {
|
||||||
|
await apiPost<{ assistant_message_id: number }>(`/api/briefing/articles/${item.id}/discuss`, { conv_id: todayConvId.value })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await chatStore.fetchConversation(todayConvId.value)
|
||||||
|
await chatStore.reconnectIfGenerating(todayConvId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RSS reactions: map of rss_item_id -> 'up' | 'down' | null
|
// RSS reactions: map of rss_item_id -> 'up' | 'down' | null
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from sqlalchemy import select
|
|||||||
from fabledassistant.auth import login_required
|
from fabledassistant.auth import login_required
|
||||||
from fabledassistant.models import async_session
|
from fabledassistant.models import async_session
|
||||||
from fabledassistant.models.conversation import Conversation, Message
|
from fabledassistant.models.conversation import Conversation, Message
|
||||||
from fabledassistant.models.rss_feed import RssFeed
|
from fabledassistant.models.rss_feed import RssFeed, RssItem
|
||||||
from fabledassistant.services import rss as rss_svc
|
from fabledassistant.services import rss as rss_svc
|
||||||
from fabledassistant.services import weather as weather_svc
|
from fabledassistant.services import weather as weather_svc
|
||||||
from fabledassistant.services.briefing_conversations import (
|
from fabledassistant.services.briefing_conversations import (
|
||||||
@@ -18,6 +18,9 @@ from fabledassistant.services.briefing_conversations import (
|
|||||||
list_briefing_conversations,
|
list_briefing_conversations,
|
||||||
post_message,
|
post_message,
|
||||||
)
|
)
|
||||||
|
from fabledassistant.services.chat import add_message, get_conversation
|
||||||
|
from fabledassistant.services.generation_buffer import create_buffer, get_buffer
|
||||||
|
from fabledassistant.services.generation_task import run_generation
|
||||||
from fabledassistant.services.settings import get_setting, set_settings_batch
|
from fabledassistant.services.settings import get_setting, set_settings_batch
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -415,3 +418,90 @@ async def list_news():
|
|||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
return jsonify({"items": items, "offset": offset, "limit": limit})
|
return jsonify({"items": items, "offset": offset, "limit": limit})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Article Discuss ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@briefing_bp.route("/articles/<int:item_id>/discuss", methods=["POST"])
|
||||||
|
@_REQUIRE
|
||||||
|
async def discuss_article(item_id: int):
|
||||||
|
"""Inject article content as a synthetic tool exchange and trigger generation."""
|
||||||
|
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 item belongs to user via feed ownership
|
||||||
|
async with async_session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(RssItem)
|
||||||
|
.join(RssFeed, RssFeed.id == RssItem.feed_id)
|
||||||
|
.where(RssItem.id == item_id, RssFeed.user_id == uid)
|
||||||
|
)
|
||||||
|
item = result.scalars().first()
|
||||||
|
if item is None:
|
||||||
|
return jsonify({"error": "Not found"}), 404
|
||||||
|
|
||||||
|
# Verify conversation belongs to user
|
||||||
|
conv = await get_conversation(uid, conv_id)
|
||||||
|
if conv is None:
|
||||||
|
return jsonify({"error": "Conversation not found"}), 404
|
||||||
|
|
||||||
|
# Reject if generation already running
|
||||||
|
if get_buffer(conv_id) is not None:
|
||||||
|
return jsonify({"error": "Generation already in progress"}), 409
|
||||||
|
|
||||||
|
article_content = item.content or ""
|
||||||
|
|
||||||
|
# Store synthetic assistant message with read_article tool result
|
||||||
|
synthetic_tool_calls = [{
|
||||||
|
"function": "read_article",
|
||||||
|
"arguments": {"url": item.url},
|
||||||
|
"result": {
|
||||||
|
"success": True,
|
||||||
|
"type": "article_content",
|
||||||
|
"url": item.url,
|
||||||
|
"content": article_content,
|
||||||
|
"truncated": False,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
await add_message(conv_id, "assistant", "", status="complete", tool_calls=synthetic_tool_calls)
|
||||||
|
|
||||||
|
# Store user message
|
||||||
|
await add_message(conv_id, "user", "Please summarize and discuss this article.")
|
||||||
|
|
||||||
|
# Reload conversation with fresh messages to build history
|
||||||
|
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 "",
|
||||||
|
"Please summarize and discuss this article.",
|
||||||
|
think=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
return jsonify({"assistant_message_id": assistant_msg.id, "status": "generating"}), 202
|
||||||
|
|||||||
Reference in New Issue
Block a user