feat(briefing): agentic compilation path behind feature flag (PR 1/N)
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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Curated read-only tool subset for agentic briefings.
|
||||
|
||||
The main chat pipeline exposes 40+ tools via ``tools.get_tools_for_user``,
|
||||
including mutating tools (``create_task``, ``delete_note``) and
|
||||
external-search tools (``search_images``, ``search_web``). Neither is
|
||||
appropriate for a scheduled background job that generates briefings —
|
||||
briefings are read-only and should not reach out to the internet on the
|
||||
user's behalf, and leaving high-noise tools in the list increases the
|
||||
chance of spurious calls (e.g. ``search_images`` firing on "what
|
||||
meeting?").
|
||||
|
||||
This module maintains an explicit allowlist. New tools added to
|
||||
``tools.py`` are not automatically exposed to briefings — they must be
|
||||
opted in by name here.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fabledassistant.services.tools import get_tools_for_user
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Explicit allowlist — tools a briefing is permitted to call. Read-only only.
|
||||
# Any tool not listed here is invisible to the briefing model.
|
||||
BRIEFING_TOOL_NAMES: frozenset[str] = frozenset({
|
||||
# Tasks — what's actionable today, overdue, or high priority
|
||||
"list_tasks",
|
||||
# Calendar — internal event store and CalDAV (if configured)
|
||||
"list_events",
|
||||
"search_events",
|
||||
# Weather — today's forecast for the user's configured locations
|
||||
"get_weather",
|
||||
# News — RSS items filtered by user preferences
|
||||
"get_rss_items",
|
||||
# Projects — context for prioritization and narrative continuity
|
||||
"list_projects",
|
||||
"search_projects",
|
||||
"get_project",
|
||||
# Notes — surface recent captures for "pick up where you left off"
|
||||
"list_notes",
|
||||
"get_note",
|
||||
})
|
||||
|
||||
|
||||
async def get_briefing_tools(user_id: int) -> list[dict]:
|
||||
"""Return the tool schemas a briefing run is permitted to call.
|
||||
|
||||
Builds the user's full tool list (so user-specific gating such as
|
||||
CalDAV availability still applies) and filters it down to the
|
||||
briefing allowlist.
|
||||
"""
|
||||
all_tools = await get_tools_for_user(user_id)
|
||||
filtered = [
|
||||
t for t in all_tools
|
||||
if t.get("function", {}).get("name") in BRIEFING_TOOL_NAMES
|
||||
]
|
||||
logger.debug(
|
||||
"Briefing tools for user %d: %d of %d selected",
|
||||
user_id, len(filtered), len(all_tools),
|
||||
)
|
||||
return filtered
|
||||
Reference in New Issue
Block a user