Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 267a975455 | |||
| 7bd1548f71 |
@@ -537,10 +537,16 @@ async def discuss_article(item_id: int):
|
|||||||
# exchange and the conversational seed user prompt into the conversation.
|
# exchange and the conversational seed user prompt into the conversation.
|
||||||
# The /news from-article route calls the same helper so behavior stays
|
# The /news from-article route calls the same helper so behavior stays
|
||||||
# byte-identical across entry points.
|
# byte-identical across entry points.
|
||||||
from fabledassistant.services.article_context import seed_article_discussion
|
from fabledassistant.services.article_context import (
|
||||||
|
EmptyArticleError,
|
||||||
|
seed_article_discussion,
|
||||||
|
)
|
||||||
|
|
||||||
model = await get_setting(uid, "default_model", "") or ""
|
model = await get_setting(uid, "default_model", "") or ""
|
||||||
discuss_prompt = await seed_article_discussion(conv_id, item, model)
|
try:
|
||||||
|
discuss_prompt = await seed_article_discussion(conv_id, item, model)
|
||||||
|
except EmptyArticleError as e:
|
||||||
|
return jsonify({"error": str(e)}), 422
|
||||||
|
|
||||||
# Reload conversation with fresh messages to build history
|
# Reload conversation with fresh messages to build history
|
||||||
conv = await get_conversation(uid, conv_id)
|
conv = await get_conversation(uid, conv_id)
|
||||||
|
|||||||
@@ -521,7 +521,10 @@ async def create_conversation_from_article(item_id: int):
|
|||||||
from sqlalchemy import select as _select
|
from sqlalchemy import select as _select
|
||||||
from fabledassistant.models import async_session as _async_session
|
from fabledassistant.models import async_session as _async_session
|
||||||
from fabledassistant.models.rss_feed import RssItem, RssFeed
|
from fabledassistant.models.rss_feed import RssItem, RssFeed
|
||||||
from fabledassistant.services.article_context import seed_article_discussion
|
from fabledassistant.services.article_context import (
|
||||||
|
EmptyArticleError,
|
||||||
|
seed_article_discussion,
|
||||||
|
)
|
||||||
|
|
||||||
uid = get_current_user_id()
|
uid = get_current_user_id()
|
||||||
|
|
||||||
@@ -540,7 +543,16 @@ async def create_conversation_from_article(item_id: int):
|
|||||||
conv = await create_conversation(uid, title=conv_title, conversation_type="chat")
|
conv = await create_conversation(uid, title=conv_title, conversation_type="chat")
|
||||||
|
|
||||||
model = await get_setting(uid, "default_model", "") or Config.OLLAMA_MODEL
|
model = await get_setting(uid, "default_model", "") or Config.OLLAMA_MODEL
|
||||||
discuss_prompt = await seed_article_discussion(conv.id, item, model)
|
try:
|
||||||
|
discuss_prompt = await seed_article_discussion(conv.id, item, model)
|
||||||
|
except EmptyArticleError as e:
|
||||||
|
# Roll back the empty conversation so the user doesn't end up with a
|
||||||
|
# phantom entry in their chat list.
|
||||||
|
try:
|
||||||
|
await delete_conversation(uid, conv.id)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to clean up empty article conversation %s", conv.id)
|
||||||
|
return jsonify({"error": str(e)}), 422
|
||||||
|
|
||||||
# Reload conversation so we see the two messages the helper just added.
|
# Reload conversation so we see the two messages the helper just added.
|
||||||
conv = await get_conversation(uid, conv.id)
|
conv = await get_conversation(uid, conv.id)
|
||||||
|
|||||||
@@ -182,6 +182,15 @@ ARTICLE_DISCUSS_SEED = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyArticleError(Exception):
|
||||||
|
"""Raised when an article has no extractable body text.
|
||||||
|
|
||||||
|
Callers (the briefing and /news discuss routes) map this to a 422 so the
|
||||||
|
user sees a clear error instead of a hallucinated summary built from an
|
||||||
|
empty synthetic tool result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def seed_article_discussion(
|
async def seed_article_discussion(
|
||||||
conv_id: int,
|
conv_id: int,
|
||||||
item: RssItem,
|
item: RssItem,
|
||||||
@@ -213,15 +222,30 @@ async def seed_article_discussion(
|
|||||||
article_content = item.context_prepared
|
article_content = item.context_prepared
|
||||||
else:
|
else:
|
||||||
raw_body = await get_or_fetch_full_article(item) or item.content or ""
|
raw_body = await get_or_fetch_full_article(item) or item.content or ""
|
||||||
|
if not raw_body.strip():
|
||||||
|
# Hard-fail rather than stage an empty synthetic tool result.
|
||||||
|
# An empty `content` field silently tells the model "the article
|
||||||
|
# has nothing in it" and it confabulates from RAG/history. Better
|
||||||
|
# to surface a clean error to the user.
|
||||||
|
logger.warning(
|
||||||
|
"Article discussion aborted: empty body for rss_item %s (%s)",
|
||||||
|
item.id, item.url,
|
||||||
|
)
|
||||||
|
raise EmptyArticleError(
|
||||||
|
"Couldn't extract any readable text from this article."
|
||||||
|
)
|
||||||
article_content = await prepare_article_context(
|
article_content = await prepare_article_context(
|
||||||
item.title or "", item.url, raw_body, model,
|
item.title or "", item.url, raw_body, model,
|
||||||
)
|
)
|
||||||
if article_content:
|
if not article_content.strip():
|
||||||
async with async_session() as session:
|
raise EmptyArticleError(
|
||||||
fresh = await session.get(RssItem, item.id)
|
"Couldn't extract any readable text from this article."
|
||||||
if fresh is not None:
|
)
|
||||||
fresh.context_prepared = article_content
|
async with async_session() as session:
|
||||||
await session.commit()
|
fresh = await session.get(RssItem, item.id)
|
||||||
|
if fresh is not None:
|
||||||
|
fresh.context_prepared = article_content
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
synthetic_tool_calls = [{
|
synthetic_tool_calls = [{
|
||||||
"function": "read_article",
|
"function": "read_article",
|
||||||
|
|||||||
@@ -724,18 +724,27 @@ async def build_context(
|
|||||||
orphan_only = rag_project_id is None
|
orphan_only = rag_project_id is None
|
||||||
effective_project_id = rag_project_id if (rag_project_id is not None and rag_project_id != -1) else None
|
effective_project_id = rag_project_id if (rag_project_id is not None and rag_project_id != -1) else None
|
||||||
|
|
||||||
try:
|
# Skip RAG auto-injection on the first turn of a seeded article discussion.
|
||||||
from fabledassistant.services.embeddings import semantic_search_notes
|
# The article body is already the sole context the user wants — pulling in
|
||||||
for score, note in await semantic_search_notes(
|
# unrelated orphan notes tricks the model into summarizing those instead.
|
||||||
user_id, user_message, exclude_ids=search_exclude or None, limit=8,
|
# Follow-up turns keep RAG on because by then the user's own messages drive
|
||||||
project_id=effective_project_id,
|
# the query rather than the generic seed prompt.
|
||||||
orphan_only=orphan_only,
|
from fabledassistant.services.article_context import ARTICLE_DISCUSS_SEED
|
||||||
):
|
_skip_rag_for_article_seed = user_message.strip() == ARTICLE_DISCUSS_SEED
|
||||||
found_scored.append((score, note))
|
|
||||||
except Exception:
|
|
||||||
logger.warning("Semantic note search failed, falling back to keyword search", exc_info=True)
|
|
||||||
|
|
||||||
if not found_scored:
|
if not _skip_rag_for_article_seed:
|
||||||
|
try:
|
||||||
|
from fabledassistant.services.embeddings import semantic_search_notes
|
||||||
|
for score, note in await semantic_search_notes(
|
||||||
|
user_id, user_message, exclude_ids=search_exclude or None, limit=8,
|
||||||
|
project_id=effective_project_id,
|
||||||
|
orphan_only=orphan_only,
|
||||||
|
):
|
||||||
|
found_scored.append((score, note))
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Semantic note search failed, falling back to keyword search", exc_info=True)
|
||||||
|
|
||||||
|
if not found_scored and not _skip_rag_for_article_seed:
|
||||||
keywords = _extract_keywords(user_message)
|
keywords = _extract_keywords(user_message)
|
||||||
if keywords:
|
if keywords:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user