feat(journal): chat model has no tools; curator runs them async (Phase 1a)
Backend half of the conversation+curator architecture (Fable #172). Decouples the journal chat surface from tool calling: the chat model now sees `tools=[]` and just talks, while a separate curator pass extracts beats and fires the tool calls. services/generation_task.py: - When conversation_type == "journal", pass `tools=[]` to Ollama regardless of what the journal tool set would normally provide. The chat model literally cannot fire record_moment / create_task / etc., so it cannot lie about firing them — the primary failure mode this architecture removes. services/curator.py (new): - `run_curator_for_conversation(conv_id, since=None)` loads recent messages, builds a curator-specific system prompt (extract beats, emit tool calls, optionally a one-line summary), and iterates the Ollama tool-call loop using the user's background_model so the chat model's KV cache survives. - Same tool registry as a normal journal conversation (record_moment, search_notes, update_task, create_task, save_person, save_place, etc.). The curator chooses naturally among them; no need for a separate curator-specific filter. - Returns CuratorRunResult with per-call status + a summary line. - Caps at 4 tool-call rounds — bounded task (extract beats from a fixed transcript), shouldn't need more. - Errors land in result.error rather than raising; the manual trigger surface (and later the scheduler) want a structured result, not exceptions. routes/journal.py: - New POST /api/journal/curator/run/<conv_id> for manual triggers. Validates conv ownership before running. Returns the CuratorRunResult dict so the UI can show what was captured. What's not in this commit (deferred to later phases): - The scheduler that auto-runs the curator (phase 2 — adds the `conversations.last_curator_run_at` column + APScheduler job). - Curator → chat feedback loop (phase 3 — summary gets injected into subsequent chat system prompts). - Right-rail captures panel in JournalView (phase 1b — pure frontend work, separate commit for clean review). - Research surface separation (phase 4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,13 @@ async def run_generation(
|
||||
buf.append_event("status", {"status": "Building context..."})
|
||||
|
||||
# Phase 1: Resolve the tools list for this user, scoped to conversation type.
|
||||
#
|
||||
# Journal conversations get NO tools (2026-05-22 architecture pivot):
|
||||
# the chat model talks, a separate background curator does tool calls
|
||||
# asynchronously. See services/curator.py. Removing tools here is the
|
||||
# mechanical change that makes the architecture real — the chat model
|
||||
# can no longer fire record_moment / create_task / etc. and therefore
|
||||
# can no longer lie about firing them.
|
||||
from fabledassistant.models import async_session as _async_session
|
||||
from fabledassistant.models.conversation import Conversation as _Conversation
|
||||
async with _async_session() as _sess:
|
||||
@@ -184,7 +191,15 @@ async def run_generation(
|
||||
_conversation_type = (
|
||||
_conv.conversation_type if _conv and _conv.conversation_type else "chat"
|
||||
)
|
||||
tools = await get_tools_for_user(user_id, conversation_type=_conversation_type)
|
||||
if _conversation_type == "journal":
|
||||
tools = []
|
||||
logger.info(
|
||||
"Conv %d is journal: passing tools=[] to chat model "
|
||||
"(curator handles tool calls async)",
|
||||
conv_id,
|
||||
)
|
||||
else:
|
||||
tools = await get_tools_for_user(user_id, conversation_type=_conversation_type)
|
||||
|
||||
logger.info(
|
||||
"Starting generation for conv %d: model=%s, tools=%d",
|
||||
|
||||
Reference in New Issue
Block a user