From a773c11aa0e981b4c9ba7a35fb37c14c8219c159 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 30 Mar 2026 15:12:38 -0400 Subject: [PATCH] feat: RSS embeddings, semantic news in chat, article-to-chat, richer briefings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Embed RSS items at fetch time (nomic-embed-text); backfill at startup - Semantic news search injected into chat system prompt ("Recent News You've Seen") when items match query above 0.55 cosine threshold (independent of note RAG) - "Discuss in chat" button on news cards — creates a seeded conversation with the article title + full content, navigates directly to the new chat - Briefing compilation now passes 500-char article excerpts (not just headlines) to the LLM and uses 8192 num_ctx to accommodate the larger prompt Co-Authored-By: Claude Sonnet 4.6 --- .../versions/0035_add_rss_item_embeddings.py | 28 ++++ frontend/src/api/client.ts | 4 + frontend/src/views/NewsView.vue | 35 +++++ src/fabledassistant/app.py | 5 + src/fabledassistant/models/__init__.py | 1 + .../models/rss_item_embedding.py | 25 ++++ src/fabledassistant/routes/chat.py | 49 +++++++ .../services/briefing_pipeline.py | 15 ++- src/fabledassistant/services/embeddings.py | 125 ++++++++++++++++++ src/fabledassistant/services/llm.py | 27 ++++ src/fabledassistant/services/rss.py | 21 ++- 11 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 alembic/versions/0035_add_rss_item_embeddings.py create mode 100644 src/fabledassistant/models/rss_item_embedding.py diff --git a/alembic/versions/0035_add_rss_item_embeddings.py b/alembic/versions/0035_add_rss_item_embeddings.py new file mode 100644 index 0000000..614ead0 --- /dev/null +++ b/alembic/versions/0035_add_rss_item_embeddings.py @@ -0,0 +1,28 @@ +"""Add rss_item_embeddings table for semantic news search.""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +revision = "0035" +down_revision = "0034" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "rss_item_embeddings", + sa.Column("rss_item_id", sa.Integer(), sa.ForeignKey("rss_items.id", ondelete="CASCADE"), primary_key=True), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("embedding", JSONB(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + ) + op.create_index("ix_rss_item_embeddings_user_id", "rss_item_embeddings", ["user_id"]) + op.create_index("ix_rss_item_embeddings_rss_item_id", "rss_item_embeddings", ["rss_item_id"]) + + +def downgrade() -> None: + op.drop_index("ix_rss_item_embeddings_rss_item_id", table_name="rss_item_embeddings") + op.drop_index("ix_rss_item_embeddings_user_id", table_name="rss_item_embeddings") + op.drop_table("rss_item_embeddings") diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 42daf6f..411ad58 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -426,6 +426,10 @@ export async function deleteRssReaction(rssItemId: number): Promise { return apiDelete(`/api/briefing/rss-reactions/${rssItemId}`); } +export async function openArticleInChat(itemId: number): Promise<{ conversation_id: number }> { + return apiPost(`/api/chat/from-article/${itemId}`, {}); +} + export async function geocodeAddress(address: string): Promise<{ lat: number; lon: number; display_name: string } | null> { try { const r = await apiPost<{ lat: number; lon: number; label: string }>('/api/briefing/weather/geocode', { query: address }); diff --git a/frontend/src/views/NewsView.vue b/frontend/src/views/NewsView.vue index cf9b241..5475a23 100644 --- a/frontend/src/views/NewsView.vue +++ b/frontend/src/views/NewsView.vue @@ -1,14 +1,18 @@