From db092b113e730c9ee4ae2ac9e28ea5175607cd0a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 5 Apr 2026 13:14:55 -0400 Subject: [PATCH] fix: rss classifier think-tag stripping, briefing calendar dict access, embed empty-string guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rss_classifier: strip ... blocks (qwen3 reasoning output) before JSON parse; use strict=False for control chars; bump timeout 30s→120s - briefing_pipeline: list_events returns dicts not Event objects — fix attribute access (.all_day/.start_dt/.title) to dict access - embeddings: guard upsert_note_embedding and semantic_search_notes against empty-string input to prevent Ollama embed 400 errors Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/services/briefing_pipeline.py | 12 ++++++++---- src/fabledassistant/services/embeddings.py | 4 ++++ src/fabledassistant/services/rss_classifier.py | 9 +++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 708c55f..95f33b7 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -172,14 +172,18 @@ async def _gather_internal(user_id: int) -> dict: user_id=user_id, date_from=day_start, date_to=day_end ) for e in internal_events: - if e.all_day: + if e.get("all_day"): time_str = "all day" - elif e.start_dt: - local_dt = e.start_dt.astimezone(user_tz) if e.start_dt.tzinfo else e.start_dt.replace(tzinfo=timezone.utc).astimezone(user_tz) + elif e.get("start_dt"): + from datetime import datetime as _dt + start = e["start_dt"] + if isinstance(start, str): + start = _dt.fromisoformat(start) + local_dt = start.astimezone(user_tz) if start.tzinfo else start.replace(tzinfo=timezone.utc).astimezone(user_tz) time_str = local_dt.strftime("%-I:%M %p") else: time_str = "unknown time" - calendar_events.append(f"{e.title} at {time_str}") + calendar_events.append(f"{e.get('title', 'Event')} at {time_str}") except Exception: logger.warning("Failed to gather internal calendar events for briefing", exc_info=True) # Also pull CalDAV events (deduped) diff --git a/src/fabledassistant/services/embeddings.py b/src/fabledassistant/services/embeddings.py index 4c2e297..fbef48d 100644 --- a/src/fabledassistant/services/embeddings.py +++ b/src/fabledassistant/services/embeddings.py @@ -63,6 +63,8 @@ def _cosine_similarity(a: list[float], b: list[float]) -> float: async def upsert_note_embedding(note_id: int, user_id: int, text: str) -> None: """Generate and persist an embedding for a note. Safe to fire-and-forget.""" + if not text or not text.strip(): + return try: embedding = await get_embedding(text) except Exception: @@ -97,6 +99,8 @@ async def semantic_search_notes( *threshold* are returned, sorted highest-first. Returns an empty list if the embedding model is unavailable or on any error. """ + if not query or not query.strip(): + return [] try: query_vec = await get_embedding(query) except Exception: diff --git a/src/fabledassistant/services/rss_classifier.py b/src/fabledassistant/services/rss_classifier.py index 00934cc..6ddbb9b 100644 --- a/src/fabledassistant/services/rss_classifier.py +++ b/src/fabledassistant/services/rss_classifier.py @@ -7,6 +7,7 @@ Called from rss.py after new items are stored — fire-and-forget. import json import logging +import re from datetime import datetime, timezone import httpx @@ -37,7 +38,7 @@ async def _llm_classify(prompt: str, model: str) -> str: "stream": False, "options": {"num_ctx": 2048, "temperature": 0.0}, } - async with httpx.AsyncClient(timeout=30.0) as client: + async with httpx.AsyncClient(timeout=120.0) as client: resp = await client.post(f"{Config.OLLAMA_URL}/api/chat", json=payload) resp.raise_for_status() return resp.json().get("message", {}).get("content", "") @@ -75,13 +76,17 @@ async def classify_items_batch( try: raw = await _llm_classify(prompt, model) + # Strip ... blocks emitted by reasoning models (e.g. qwen3) + raw = re.sub(r".*?", "", raw, flags=re.DOTALL) # Extract JSON from response (LLM may wrap it in markdown) raw = raw.strip() if raw.startswith("```"): raw = raw.split("```")[1] if raw.startswith("json"): raw = raw[4:] - parsed = json.loads(raw) + raw = raw.strip() + # Allow control characters that may appear in LLM-generated JSON strings + parsed = json.loads(raw, strict=False) return {int(k): v for k, v in parsed.items() if isinstance(v, list)} except Exception: logger.warning("RSS classification failed", exc_info=True)