fix: rss classifier think-tag stripping, briefing calendar dict access, embed empty-string guard
- rss_classifier: strip <think>...</think> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <think>...</think> blocks emitted by reasoning models (e.g. qwen3)
|
||||
raw = re.sub(r"<think>.*?</think>", "", 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)
|
||||
|
||||
Reference in New Issue
Block a user