"""Honcho tools for user context retrieval. Registers three complementary tools, ordered by capability: honcho_context — dialectic Q&A (LLM-powered, direct answers) honcho_search — semantic search (fast, no LLM, raw excerpts) honcho_profile — peer card (fast, no LLM, structured facts) Use honcho_context when you need Honcho to synthesize an answer. Use honcho_search or honcho_profile when you want raw data to reason over yourself. The session key is injected at runtime by the agent loop via ``set_session_context()``. """ import json import logging logger = logging.getLogger(__name__) # ── Module-level state (injected by AIAgent at init time) ── _session_manager = None # HonchoSessionManager instance _session_key: str | None = None # Current session key (e.g., "telegram:123456") def set_session_context(session_manager, session_key: str) -> None: """Register the active Honcho session manager and key. Called by AIAgent.__init__ when Honcho is enabled. """ global _session_manager, _session_key _session_manager = session_manager _session_key = session_key def clear_session_context() -> None: """Clear session context (for testing or shutdown).""" global _session_manager, _session_key _session_manager = None _session_key = None # ── Availability check ── def _check_honcho_available() -> bool: """Tool is available when Honcho is active OR configured. At banner time the session context hasn't been injected yet, but if a valid config exists the tools *will* activate once the agent starts. Returning True for "configured" prevents the banner from marking honcho tools as red/disabled when they're actually going to work. """ # Fast path: session already active (mid-conversation) if _session_manager is not None and _session_key is not None: return True # Slow path: check if Honcho is configured (banner time) try: from honcho_integration.client import HonchoClientConfig cfg = HonchoClientConfig.from_global_config() return cfg.enabled and bool(cfg.api_key or cfg.base_url) except Exception: return False def _resolve_session_context(**kwargs): """Prefer the calling agent's session context over module-global fallback.""" session_manager = kwargs.get("honcho_manager") or _session_manager session_key = kwargs.get("honcho_session_key") or _session_key return session_manager, session_key # ── honcho_profile ── _PROFILE_SCHEMA = { "name": "honcho_profile", "description": ( "Retrieve the user's peer card from Honcho — a curated list of key facts " "about them (name, role, preferences, communication style, patterns). " "Fast, no LLM reasoning, minimal cost. " "Use this at conversation start or when you need a quick factual snapshot. " "Use honcho_context instead when you need Honcho to synthesize an answer." ), "parameters": { "type": "object", "properties": {}, "required": [], }, } def _handle_honcho_profile(args: dict, **kw) -> str: session_manager, session_key = _resolve_session_context(**kw) if not session_manager or not session_key: return json.dumps({"error": "Honcho is not active for this session."}) try: card = session_manager.get_peer_card(session_key) if not card: return json.dumps({"result": "No profile facts available yet. The user's profile builds over time through conversations."}) return json.dumps({"result": card}) except Exception as e: logger.error("Error fetching Honcho peer card: %s", e) return json.dumps({"error": f"Failed to fetch profile: {e}"}) # ── honcho_search ── _SEARCH_SCHEMA = { "name": "honcho_search", "description": ( "Semantic search over Honcho's stored context about the user. " "Returns raw excerpts ranked by relevance to your query — no LLM synthesis. " "Cheaper and faster than honcho_context. " "Good when you want to find specific past facts and reason over them yourself. " "Use honcho_context when you need a direct synthesized answer." ), "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "What to search for in Honcho's memory (e.g. 'programming languages', 'past projects', 'timezone').", }, "max_tokens": { "type": "integer", "description": "Token budget for returned context (default 800, max 2000).", }, }, "required": ["query"], }, } def _handle_honcho_search(args: dict, **kw) -> str: query = args.get("query", "") if not query: return json.dumps({"error": "Missing required parameter: query"}) session_manager, session_key = _resolve_session_context(**kw) if not session_manager or not session_key: return json.dumps({"error": "Honcho is not active for this session."}) max_tokens = min(int(args.get("max_tokens", 800)), 2000) try: result = session_manager.search_context(session_key, query, max_tokens=max_tokens) if not result: return json.dumps({"result": "No relevant context found."}) return json.dumps({"result": result}) except Exception as e: logger.error("Error searching Honcho context: %s", e) return json.dumps({"error": f"Failed to search context: {e}"}) # ── honcho_context (dialectic — LLM-powered) ── _QUERY_SCHEMA = { "name": "honcho_context", "description": ( "Ask Honcho a natural language question and get a synthesized answer. " "Uses Honcho's LLM (dialectic reasoning) — higher cost than honcho_profile or honcho_search. " "Can query about any peer: the user (default), the AI assistant, or any named peer. " "Examples: 'What are the user's main goals?', 'What has hermes been working on?', " "'What is the user's technical expertise level?'" ), "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "A natural language question.", }, "peer": { "type": "string", "description": "Which peer to query about: 'user' (default) or 'ai'. Omit for user.", }, }, "required": ["query"], }, } def _handle_honcho_context(args: dict, **kw) -> str: query = args.get("query", "") if not query: return json.dumps({"error": "Missing required parameter: query"}) session_manager, session_key = _resolve_session_context(**kw) if not session_manager or not session_key: return json.dumps({"error": "Honcho is not active for this session."}) peer_target = args.get("peer", "user") try: result = session_manager.dialectic_query(session_key, query, peer=peer_target) return json.dumps({"result": result or "No result from Honcho."}) except Exception as e: logger.error("Error querying Honcho context: %s", e) return json.dumps({"error": f"Failed to query context: {e}"}) # ── honcho_conclude ── _CONCLUDE_SCHEMA = { "name": "honcho_conclude", "description": ( "Write a conclusion about the user back to Honcho's memory. " "Conclusions are persistent facts that build the user's profile — " "preferences, corrections, clarifications, project context, or anything " "the user tells you that should be remembered across sessions. " "Use this when the user explicitly states a preference, corrects you, " "or shares something they want remembered. " "Examples: 'User prefers dark mode', 'User's project uses Python 3.11', " "'User corrected: their name is spelled Eri not Eric'." ), "parameters": { "type": "object", "properties": { "conclusion": { "type": "string", "description": "A factual statement about the user to persist in memory.", } }, "required": ["conclusion"], }, } def _handle_honcho_conclude(args: dict, **kw) -> str: conclusion = args.get("conclusion", "") if not conclusion: return json.dumps({"error": "Missing required parameter: conclusion"}) session_manager, session_key = _resolve_session_context(**kw) if not session_manager or not session_key: return json.dumps({"error": "Honcho is not active for this session."}) try: ok = session_manager.create_conclusion(session_key, conclusion) if ok: return json.dumps({"result": f"Conclusion saved: {conclusion}"}) return json.dumps({"error": "Failed to save conclusion."}) except Exception as e: logger.error("Error creating Honcho conclusion: %s", e) return json.dumps({"error": f"Failed to save conclusion: {e}"}) # ── Registration ── from tools.registry import registry registry.register( name="honcho_profile", toolset="honcho", schema=_PROFILE_SCHEMA, handler=_handle_honcho_profile, check_fn=_check_honcho_available, emoji="🔮", ) registry.register( name="honcho_search", toolset="honcho", schema=_SEARCH_SCHEMA, handler=_handle_honcho_search, check_fn=_check_honcho_available, emoji="🔮", ) registry.register( name="honcho_context", toolset="honcho", schema=_QUERY_SCHEMA, handler=_handle_honcho_context, check_fn=_check_honcho_available, emoji="🔮", ) registry.register( name="honcho_conclude", toolset="honcho", schema=_CONCLUDE_SCHEMA, handler=_handle_honcho_conclude, check_fn=_check_honcho_available, emoji="🔮", )