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:
@@ -171,6 +171,39 @@ async def list_days():
|
||||
return jsonify({"days": [d.isoformat() for d in rows]})
|
||||
|
||||
|
||||
@journal_bp.post("/curator/run/<int:conv_id>")
|
||||
@login_required
|
||||
async def trigger_curator_run(conv_id: int):
|
||||
"""Manually run the journal curator over a conversation.
|
||||
|
||||
The curator reads recent messages and fires tool calls (record_moment,
|
||||
update_task, etc.) the chat model can't (chat models have tools=[]).
|
||||
Returns a summary of what was captured.
|
||||
|
||||
See services/curator.py for the architectural background.
|
||||
"""
|
||||
user_id = get_current_user_id()
|
||||
|
||||
# Confirm the conversation belongs to this user (curator runs against
|
||||
# arbitrary conv_ids would otherwise leak data across tenants).
|
||||
from sqlalchemy import select as _select
|
||||
from fabledassistant.models import async_session as _async_session
|
||||
from fabledassistant.models.conversation import Conversation as _Conversation
|
||||
async with _async_session() as _sess:
|
||||
_res = await _sess.execute(
|
||||
_select(_Conversation).where(
|
||||
_Conversation.id == conv_id,
|
||||
_Conversation.user_id == user_id,
|
||||
)
|
||||
)
|
||||
if _res.scalar_one_or_none() is None:
|
||||
return jsonify({"error": "Conversation not found"}), 404
|
||||
|
||||
from fabledassistant.services.curator import run_curator_for_conversation
|
||||
result = await run_curator_for_conversation(conv_id)
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
@journal_bp.post("/trigger-prep")
|
||||
@login_required
|
||||
async def trigger_prep():
|
||||
|
||||
Reference in New Issue
Block a user