Merge pull request 'Release v26.04.14.2 — Discuss failure mode fixes' (#33) from dev into main
This commit was merged in pull request #33.
This commit is contained in:
@@ -537,10 +537,16 @@ async def discuss_article(item_id: int):
|
||||
# exchange and the conversational seed user prompt into the conversation.
|
||||
# The /news from-article route calls the same helper so behavior stays
|
||||
# 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 ""
|
||||
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
|
||||
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 fabledassistant.models import async_session as _async_session
|
||||
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()
|
||||
|
||||
@@ -540,7 +543,16 @@ async def create_conversation_from_article(item_id: int):
|
||||
conv = await create_conversation(uid, title=conv_title, conversation_type="chat")
|
||||
|
||||
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.
|
||||
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(
|
||||
conv_id: int,
|
||||
item: RssItem,
|
||||
@@ -213,15 +222,30 @@ async def seed_article_discussion(
|
||||
article_content = item.context_prepared
|
||||
else:
|
||||
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(
|
||||
item.title or "", item.url, raw_body, model,
|
||||
)
|
||||
if article_content:
|
||||
async with async_session() as session:
|
||||
fresh = await session.get(RssItem, item.id)
|
||||
if fresh is not None:
|
||||
fresh.context_prepared = article_content
|
||||
await session.commit()
|
||||
if not article_content.strip():
|
||||
raise EmptyArticleError(
|
||||
"Couldn't extract any readable text from this article."
|
||||
)
|
||||
async with async_session() as session:
|
||||
fresh = await session.get(RssItem, item.id)
|
||||
if fresh is not None:
|
||||
fresh.context_prepared = article_content
|
||||
await session.commit()
|
||||
|
||||
synthetic_tool_calls = [{
|
||||
"function": "read_article",
|
||||
|
||||
@@ -724,18 +724,27 @@ async def build_context(
|
||||
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
|
||||
|
||||
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)
|
||||
# Skip RAG auto-injection on the first turn of a seeded article discussion.
|
||||
# The article body is already the sole context the user wants — pulling in
|
||||
# unrelated orphan notes tricks the model into summarizing those instead.
|
||||
# Follow-up turns keep RAG on because by then the user's own messages drive
|
||||
# the query rather than the generic seed prompt.
|
||||
from fabledassistant.services.article_context import ARTICLE_DISCUSS_SEED
|
||||
_skip_rag_for_article_seed = user_message.strip() == ARTICLE_DISCUSS_SEED
|
||||
|
||||
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)
|
||||
if keywords:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user