feat(journal): LLM tools (record_moment, search_journal) + system prompt wiring

- services/tools/journal.py — record_moment + search_journal tool handlers
- services/tools/_registry.py: add `journal` flag on ToolDef + tool() decorator
- get_tools_for_user(user_id, conversation_type='chat'|'journal') —
  exclude journal-only tools from chat sessions; exclude set_rag_scope
  from journal sessions
- services/tools/__init__.py: register the new journal module; drop the
  unused get_briefing_tools export
- services/llm.py build_context: short-circuit for journal conversations,
  using journal_pipeline.build_journal_system_prompt and skipping all
  notes-RAG injection (preserves the journal/notes isolation invariant)
- services/generation_task.py: pass conversation_type into get_tools_for_user

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 22:39:42 -04:00
co-authored by Claude Opus 4.7
parent d9ab538ef4
commit ac188c40a5
5 changed files with 203 additions and 18 deletions
+19 -14
View File
@@ -27,6 +27,7 @@ class ToolDef:
handler: ToolHandler
read_only: bool = False
briefing: bool = False
journal: bool = False
requires: str | None = None
required_params: list[str] = field(default_factory=list)
@@ -57,6 +58,7 @@ def tool(
required: list[str] | None = None,
read_only: bool = False,
briefing: bool = False,
journal: bool = False,
requires: str | None = None,
) -> Callable[[ToolHandler], ToolHandler]:
"""Register an async tool handler with its schema metadata."""
@@ -71,6 +73,7 @@ def tool(
handler=fn,
read_only=read_only,
briefing=briefing,
journal=journal,
requires=requires,
required_params=required or [],
)
@@ -90,27 +93,29 @@ async def _check_requires(user_id: int, requires: str) -> bool:
return True
async def get_tools_for_user(user_id: int) -> list[dict]:
"""Build the tool schema list for a user based on configured integrations."""
async def get_tools_for_user(
user_id: int, *, conversation_type: str = "chat"
) -> list[dict]:
"""Build the tool schema list for a user, scoped by conversation type.
- 'chat': all tools except those marked journal-only.
- 'journal': all tools except set_rag_scope (scope is implicit in journal).
"""
tools: list[dict] = []
for td in _REGISTRY.values():
if td.requires and not await _check_requires(user_id, td.requires):
continue
if conversation_type == "journal":
if td.name == "set_rag_scope":
continue
else:
if td.journal:
continue
tools.append(td.schema())
logger.debug("User %d: %d tools available", user_id, len(tools))
return tools
async def get_briefing_tools(user_id: int) -> list[dict]:
"""Return only the tool schemas marked ``briefing=True``."""
all_tools = await get_tools_for_user(user_id)
names = {td.name for td in _REGISTRY.values() if td.briefing}
filtered = [t for t in all_tools if t["function"]["name"] in names]
logger.debug(
"Briefing tools for user %d: %d of %d selected",
user_id, len(filtered), len(all_tools),
"User %d / %s: %d tools available", user_id, conversation_type, len(tools)
)
return filtered
return tools
async def execute_tool(