Split 2566-line tools.py into a tools/ package with @tool decorator
registration. Each tool's schema, metadata, and implementation live
together. Briefing eligibility is now a briefing=True flag instead of
a separate frozenset allowlist. Conditional inclusion (CalDAV, SearXNG)
uses requires= metadata. Public API (get_tools_for_user, execute_tool)
unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three briefing quality fixes surfaced by reading today's 2026-04-11
compilation output:
- **Stale weather**: get_weather was returning 48h-old cache data
after a missed scheduler run. Tool now auto-refreshes any cached
location older than 6h (fetching fresh data from Open-Meteo), and
stamps each location with cache_age_hours + is_stale so the model
can hedge instead of faithfully relaying old numbers.
- **Cancelled tasks leaking into prose**: briefing loop now defaults
list_tasks calls to status=["todo","in_progress"] when the model
doesn't specify, so cancelled/done items stop showing up in the
summary. Localized to the briefing path — chat still sees full
history.
- **Overdue in-progress tasks missed by midday check-in**: tightened
the check-in prompt to explicitly require two list_tasks calls —
one for in_progress (catches items dragging past their due date)
and one filtered by due date — so long-running tasks stop getting
silently dropped.
The compilation prompt mentioned "news themes" but didn't name the
tool, and the model was never calling get_rss_items. Result: today's
briefing had zero news coverage despite the tool being wired up and
in the allowlist.
- Explicitly list the tools to call in the compilation prompt so
get_rss_items gets invoked alongside list_tasks/list_events/get_weather.
- When the model calls get_rss_items during a compilation run,
intercept and return the already-scored/filtered items (topic prefs
+ reaction-weighted) instead of the raw feed dump execute_tool
would return. Aligns the model's view of news with the sidebar's
rss_item_ids metadata.
Deletes ~760 lines of legacy briefing code: format_task, compute_task_hash,
upsert_task_snapshots, _gather_internal, _gather_weekly_review,
_llm_synthesise, and the unified prompt helpers. run_compilation and
run_slot_injection are now agentic-tool-use-loop only.
briefing_scheduler and user_profile migrated from the deleted helper to
services.llm.generate_completion (retry + keep_alive baked in).
routes/briefing.manual_trigger now persists agentic tool-call receipts
via _persist_agentic_messages (previously silently dropped them) and
adds POST /api/briefing/reset-today to wipe today's briefing messages.
BREAKING: briefing_mode setting no longer honored; no legacy fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
list_notes gains exclude_paused_projects; list_tasks tool sets it when
no explicit project filter is given. Paused projects no longer leak
tasks into briefings or list_tasks results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Applies the grounding discipline from the agentic briefing work to the
main chat system prompt. The regular chat pipeline was already agentic
(it uses stream_chat_with_tools), but its system prompt never told the
model "only assert facts from tool results" or "if a tool returns
nothing, say so honestly." That left room for the same class of
hallucinations the briefings had — calling list_events, getting an
empty array, and then confidently mentioning a meeting anyway.
Adds two new static rules to the tool guidance block in llm.build_context:
GROUNDING — when the user asks about their own data, call the relevant
tool to see what exists. Never assert from memory or assumption.
HONESTY WHEN EMPTY — if a tool returns empty results, tell the user
plainly. No fabricated example items, no invented meetings, no generic
suggestions dressed up as real data.
Both rules are in the static (KV-cache-stable) portion of the system
prompt so they cost nothing on repeated requests for the same user.
Carries the hallucination fix from the briefing work directly into
every chat turn, not just chat that happens inside a briefing thread.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR 1.5 + PR 2 of the agentic briefing rollout. Extends the agentic
path to cover the morning/midday/afternoon check-in slots in addition
to the 4am compilation, and persists the full tool-call sequence
into the briefing conversation so chat follow-ups see the receipts.
run_slot_injection now honors the briefing_mode setting the same way
run_compilation does: agentic mode routes through run_agentic_briefing
with a check-in system prompt variant, legacy falls back automatically
if the new path returns empty. Signature changed from str to
(str, dict) to surface the agentic message list to the caller.
_agentic_system_prompt now takes the user's local-day window and
timezone, pre-computed by run_agentic_briefing, and embeds them in
the prompt. This eliminates a whole class of "wrong day" bugs that
would otherwise happen when the model tried to translate "today" into
ISO 8601 ranges without knowing the user's timezone.
briefing_scheduler._run_slot_for_user now calls _persist_agentic_messages
after each slot, which walks the agentic message list and stores
every intermediate assistant turn (with its tool calls and results
folded into the flat storage format the existing chat loader expects)
as a real message row. The synthetic "[Midday briefing update]"
user-role messages are no longer created — final prose is written
with metadata.briefing_slot so the UI can still identify it as a
scheduled entry. The agentic user-trigger ("Generate my morning
briefing…") is deliberately skipped so it doesn't recreate the same
fake-user-message problem we're trying to remove.
briefing_conversations.post_message now accepts tool_calls, matching
the schema the Message model already supports. This lets scheduled
briefings write structured tool-use history without reaching into
the model layer.
Net effect with briefing_mode="agentic" on:
- All four slots are grounded in tool results, no more hallucinated
events or tasks
- Chat follow-ups in the briefing conversation see morning's
list_events → [] receipt (and everything else), so "what meeting?"
gets an honest "nothing on the calendar" reply grounded in data
- No more fake [Briefing update] user messages in the chat scroll
Still to come (PR 3): UI polish — tool-call status row, inline cards
for list_events/list_tasks results, visual treatment for slot messages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
services/events.py::list_events treated any event with end_dt IS NULL
as perpetually matching, so a point-event created last week would be
returned as "happening today" whenever the briefing or list_events
tool queried today's window. That manifested as briefings claiming
past events were scheduled for the current day.
Split the null-end branch: point events now only match when start_dt
falls within [date_from, date_to]. Events with an end_dt continue to
use standard overlap logic.
This bug affected both the legacy briefing synthesis and the agentic
list_events tool path, since both call the same events service.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First cut of the agentic briefing redesign. Morning compilation can now
route through a tool-call loop that grounds every factual claim in an
actual tool result, eliminating the hallucinated meetings, tasks, and
news items the legacy one-shot path was producing. Behind a per-user
`briefing_mode` setting (default "legacy"); falls back to the legacy
path automatically if the new path returns empty (e.g. model too weak
to drive tool calls reliably).
New: services/briefing_tools.py — explicit read-only allowlist of 10
tools (tasks, events, weather, rss, projects, notes). New tools added
to tools.py must be opted in by name. Excludes all mutating tools and
external search tools (search_images, search_web, research_topic) which
are neither useful nor safe for a scheduled background job.
New: briefing_pipeline.run_agentic_briefing — wraps the existing
stream_chat_with_tools loop with slot-specific system prompts that tell
the model to only assert facts from tool results and to be honest when
tools return nothing. Max 8 rounds, per-round exception handling,
returns the full message list so tool-call receipts can be persisted
alongside the prose in a later PR.
Sentence-count floors bumped: compilation 6–10 (was 4–8), check-ins
3–5 (was 2–3). Weekly review 5–8.
Design doc: docs/2026-04-10-agentic-briefing-design.md
Out of scope for this PR (future PRs): slot-injection migration,
persisting tool-call receipts into the conversation so chat follow-ups
see them, UI polish for tool-call status, sidecar storage for
briefings. See the design doc's migration path for details.
Enable on an account with:
UPDATE settings SET value='agentic'
WHERE user_id=<id> AND key='briefing_mode';
or insert the row if missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hardcoded "2h" keep_alive everywhere with a helper that
returns OLLAMA_KEEP_ALIVE_MAIN (default 30m) for the interactive model
and OLLAMA_KEEP_ALIVE_BACKGROUND (default 10m) for the background
model. Lets the main model release VRAM during long idle periods
while keeping it warm enough for bursty chat use, and stops the
sporadic background model from camping VRAM it rarely needs.
Seven call sites updated to route through llm.keep_alive_for(model):
the streaming helpers, generate_completion, the two startup warmers,
the settings KV-cache primer, and the chat warmer endpoint.
Override via env vars: OLLAMA_KEEP_ALIVE_MAIN, OLLAMA_KEEP_ALIVE_BACKGROUND.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace indigo (#6366f1) with deep violet (#7c3aed) gradient header,
violet-tinted card border and background, refined border radius and
spacing, and violet-accented footer to match the app's design language.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes birthday, organization, address (person) and website, category (place)
from entity_meta in the knowledge API response; updates KnowledgeView cards
to display organization and category as visible meta chips/text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The model was hallucinating project names from task/note content (e.g.
inferring "Vehicle Maintenance" from "purchase wheel bearings"). Added
explicit guidance to both project field descriptions: only set if the
user explicitly named a project, never infer from content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- build_context: when conversation_type is 'briefing', inject a system
prompt instruction telling the model to answer from conversation history
and article context instead of searching the web
- Consolidate briefing conversation type detection to one DB query (was
being checked twice — once for the system prompt addition, once for
article context injection)
- ChatPanel: render a visual 'New Briefing Update' separator line before
2nd+ briefing slot messages (identified by metadata.rss_item_ids)
- types/chat.ts: add metadata field to Message interface
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single monolithic research note with topic-driven section notes
plus an index note. Two new LLM calls: _generate_outline (JSON outline, 3-8
sections) and _synthesize_section (300-600 word focused note per section,
parallelised via asyncio.gather). Public signature of run_research_pipeline
unchanged; falls back to single-note synthesis on outline failure or if all
sections fail.
Also extracts _build_sources_block helper and adds full test suite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Route now logs every synthesis request (char count, voice, speed)
- Route logs char count + text preview when the 8000-char limit is hit
- Route logs empty audio with preview (helps spot no-chunk-produced edge case)
- Route logs success with byte count and duration
- Kokoro synthesise() logs per-call: samples produced, elapsed, chars/s
- Kokoro synthesise() logs warning when zero audio chunks returned with preview
- Kokoro synthesise() catches and logs pipeline-internal errors with preview
- Frontend: console.warn now includes char count + 80-char preview on failure and retry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CONTENT_MAX_CHARS was removed from rss.py when the article content cap
was lifted. backfill_rss_article_content still referenced it, causing an
ImportError on startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When user_timezone is saved via PUT /api/settings, immediately call
update_user_schedule if briefing is enabled so the scheduler picks up
the new timezone without requiring a restart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _add_user_jobs now accepts config dict and skips disabled slots
- _get_briefing_enabled_users returns 3-tuple (user_id, tz, config)
- update_user_schedule passes config through to _add_user_jobs
- _run_slot_for_user skips morning slot on non-work days via profile.work_schedule
- Add tests for slot gating and work-day gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the custom classify_capture_intent + _process_note two-pass
approach. The LLM now picks the right tool directly via Ollama's native
tool_calls API (same path as the main chat pipeline). _should_think
decides whether extended reasoning is needed based on input length/
complexity. intent.py deleted — no longer needed.
Android app and response format unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/briefing/articles/<id>/discuss injects stored article content
as a persisted read_article tool exchange before triggering generation.
The LLM sees the article as already read; follow-ups retain context via
the fixed history builder. Frontend Discuss button now calls the new
endpoint instead of inlining article text in the user message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Needed by the Discuss endpoint to persist synthetic read_article
tool exchanges before triggering generation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The history builder was silently dropping tool_calls from prior turns,
causing the LLM to lose article/search context on every follow-up.
Now reconstructs the assistant tool_call dict + per-call tool result
entries. Messages without tool_calls are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trafilatura extracts only article body text (typically 2K–15K chars),
so storing the full content is safe without an artificial ceiling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Startup now pulls Config.OLLAMA_MODEL (system default chat model) — previously only
embedding and background models were pulled; the primary chat model was skipped
- _warm_user_models expanded to also pull user-configured default_model and
background_model overrides that are missing from Ollama, rather than logging and
skipping them; pulls run before warm/KV-cache priming
- Add background_model to _MODEL_KEYS in settings route so clearing the dropdown
deletes the DB row instead of saving "", which caused Ollama failures in tag
suggestions, title generation, project summaries, and RSS classification
- Add http/https scheme validation to PUT /api/admin/base-url matching the CalDAV
route pattern; a bad value no longer silently breaks invite/password-reset links
- Update admin voice config description: "Reload models" button exists to avoid
a server restart, so the old "restart required" text was misleading
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>