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)