"""LLM tools for journal conversations: record_moment and search_journal.""" from __future__ import annotations import datetime import logging from typing import Any from fabledassistant.services.journal_search import search_journal as svc_search_journal from fabledassistant.services.moments import create_moment from fabledassistant.services.tools._registry import tool logger = logging.getLogger(__name__) @tool( name="record_moment", description=( "Record a meaningful beat from the conversation as a structured Moment. " "Use freely (no confirmation) for anything significant the user mentions: " "events, encounters, decisions, observations, feelings worth remembering. " "Each Moment is one or two sentences distilling the beat — not a full transcript. " "Link people/places/tasks/notes by ID when the user has mentioned them." ), parameters={ "content": { "type": "string", "description": "1-2 sentence distillation of the moment in the user's voice or third-person.", }, "occurred_at": { "type": "string", "description": "ISO 8601 datetime when the moment happened. Pass 'now' for the present moment; the handler resolves it.", }, "raw_excerpt": { "type": "string", "description": "Optional: literal user phrase the moment summarizes. Useful for trust UI.", }, "tags": { "type": "array", "items": {"type": "string"}, "description": "Optional tags (no # prefix).", }, "person_ids": { "type": "array", "items": {"type": "integer"}, "description": "Person IDs (note IDs with note_type='person') mentioned in this moment.", }, "place_ids": { "type": "array", "items": {"type": "integer"}, "description": "Place IDs (note IDs with note_type='place') mentioned in this moment.", }, "task_ids": { "type": "array", "items": {"type": "integer"}, "description": "Task IDs this moment references.", }, "note_ids": { "type": "array", "items": {"type": "integer"}, "description": "Note IDs this moment references.", }, }, required=["content"], read_only=False, journal=True, ) async def record_moment_tool(*, user_id, arguments, conv_id=None, **_ctx): content = arguments.get("content", "").strip() if not content: return {"success": False, "error": "content is required"} occurred_raw = arguments.get("occurred_at") now = datetime.datetime.now(datetime.timezone.utc) if occurred_raw and occurred_raw != "now": try: occurred_dt = datetime.datetime.fromisoformat(occurred_raw) if occurred_dt.tzinfo is None: occurred_dt = occurred_dt.replace(tzinfo=datetime.timezone.utc) except ValueError: occurred_dt = now else: occurred_dt = now moment = await create_moment( user_id=user_id, content=content, occurred_at=occurred_dt, day_date=occurred_dt.date(), conversation_id=conv_id, raw_excerpt=arguments.get("raw_excerpt"), tags=arguments.get("tags") or [], person_ids=arguments.get("person_ids") or [], place_ids=arguments.get("place_ids") or [], task_ids=arguments.get("task_ids") or [], note_ids=arguments.get("note_ids") or [], ) return { "success": True, "type": "moment_recorded", "moment": moment.to_dict(), } @tool( name="search_journal", description=( "Search the user's journal: past Moments and (optionally) raw transcripts. " "Use to recall things mentioned in prior days. Three modes: " "(a) date filter only -> recent moments in that range; " "(b) person_id/place_id filter -> moments mentioning that entity; " "(c) query string -> semantic search, optionally constrained by date/entity." ), parameters={ "query": {"type": "string", "description": "Optional semantic query."}, "person_id": {"type": "integer", "description": "Filter to this person."}, "place_id": {"type": "integer", "description": "Filter to this place."}, "tag": {"type": "string", "description": "Filter to moments with this tag."}, "date_from": {"type": "string", "description": "ISO date lower bound (inclusive)."}, "date_to": {"type": "string", "description": "ISO date upper bound (inclusive)."}, "include_transcripts": { "type": "boolean", "description": "If true, also return matching raw transcript excerpts when query is set. Default false.", }, "limit": {"type": "integer", "description": "Max results, default 10."}, }, required=[], read_only=True, journal=True, ) async def search_journal_tool(*, user_id, arguments, **_ctx) -> dict[str, Any]: df_raw = arguments.get("date_from") dt_raw = arguments.get("date_to") df = datetime.date.fromisoformat(df_raw) if df_raw else None dt = datetime.date.fromisoformat(dt_raw) if dt_raw else None results = await svc_search_journal( user_id=user_id, query=arguments.get("query"), person_id=arguments.get("person_id"), place_id=arguments.get("place_id"), tag=arguments.get("tag"), date_from=df, date_to=dt, include_transcripts=bool(arguments.get("include_transcripts", False)), limit=int(arguments.get("limit", 10)), ) return {"success": True, "type": "journal_search_results", "count": len(results), "results": results}