dbd9f00061
Removes the entire RSS feature surface — feeds, items, embeddings, reactions, discussion-note flow, briefing news context, settings, env-vars, and DB tables. Keeps the URL-generic article-reader (the read_article LLM tool) under a clean module so the LLM can still fetch arbitrary article content from URLs the user provides. Backend: - New services/article_fetcher.py — single source of trafilatura URL→text - New services/tools/article.py — read_article tool (was nested under tools/rss) - Delete services/rss.py, rss_classifier.py, rss_filtering.py, article_context.py - Delete services/tools/rss.py - Delete models/rss_feed.py (RssFeed, RssItem), models/rss_item_embedding.py - services/embeddings.py: drop upsert/semantic_search/backfill RSS helpers - services/llm.py: remove _build_briefing_article_context, briefing-conv branch, ARTICLE_DISCUSS_SEED skip-RAG branch; drop get_rss_items / add_rss_feed from the actions list - services/generation_task.py: drop _maybe_save_article_discussion_note + caller - routes/chat.py: drop /api/chat/from-article/<id> endpoint - routes/journal.py: re-import via web.py refactor (article_fetcher path) - services/tools/__init__.py: register `article`, drop `rss` - services/tools/_registry.py: drop the requires=='rss' check - app.py: drop backfill_rss_item_embeddings + backfill_rss_article_content tasks - config.py: prose-only edit (no env var change — RSS env vars were never first-class) Frontend: - stores/settings.ts: drop rssEnabled - SettingsView.vue: drop the RSS-classification mention - api/client.ts: drop openArticleInChat (the from-article endpoint is gone) Tests: - Delete tests/test_rss_service.py, test_news_api.py, test_article_reading.py Migration: - 0042_drop_rss: DROP TABLE rss_item_embeddings, rss_item_reactions, rss_items, rss_feeds; DELETE settings rows for rss_enabled / briefing_*_topics Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
"""Generic article-text fetcher.
|
|
|
|
Fetches a URL and extracts its main body via trafilatura. The single source
|
|
of truth for article-content extraction across the codebase — used by the
|
|
``read_article`` LLM tool and the ``lookup`` tool's web-result enrichment.
|
|
|
|
Trafilatura/lxml is NOT safe to call concurrently — running it via
|
|
``run_in_executor`` from multiple coroutines can trip a libxml2 double-free.
|
|
Callers must serialize their fetches (await one before starting the next).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def fetch_article_text(url: str) -> str | None:
|
|
"""Return the clean article body for *url*, or None on failure.
|
|
|
|
Returns None when the HTTP fetch fails or trafilatura yields nothing
|
|
useful. Callers should treat None as "no article content available."
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True, headers={
|
|
"User-Agent": "Mozilla/5.0 (compatible; FabledScribe/1.0; +https://fabledsword.com)",
|
|
}) as client:
|
|
resp = await client.get(url)
|
|
resp.raise_for_status()
|
|
raw_html = resp.text
|
|
except Exception:
|
|
logger.debug("Failed to fetch article URL %s", url)
|
|
return None
|
|
|
|
loop = asyncio.get_event_loop()
|
|
try:
|
|
import trafilatura
|
|
text = await loop.run_in_executor(
|
|
None,
|
|
lambda: trafilatura.extract(
|
|
raw_html,
|
|
include_comments=False,
|
|
include_tables=True,
|
|
favor_recall=True,
|
|
),
|
|
)
|
|
return text or None
|
|
except Exception:
|
|
logger.debug("trafilatura extraction failed for %s", url, exc_info=True)
|
|
return None
|