"""Hypothesis Agent — analyzes phenomena and generates investigative hypotheses.""" from __future__ import annotations import json import logging from base_agent import BaseAgent from evidence_graph import EvidenceGraph, HYPOTHESIS_EDGE_WEIGHTS from llm_client import LLMClient logger = logging.getLogger(__name__) class HypothesisAgent(BaseAgent): name = "hypothesis" role = ( "Hypothesis analyst. You review all phenomena discovered so far " "and formulate investigative hypotheses about what happened on this system. " "Your ultimate goal: build the most complete picture of events that occurred. " "For each hypothesis, identify which existing phenomena support or contradict it." ) def __init__(self, llm: LLMClient, graph: EvidenceGraph) -> None: super().__init__(llm, graph) self._register_hypothesis_tools() def _register_hypothesis_tools(self) -> None: """Register hypothesis-specific tools.""" valid_edge_types = list(HYPOTHESIS_EDGE_WEIGHTS.keys()) self.register_tool( name="add_hypothesis", description=( "Create a new investigative hypothesis about what happened on the system. " "Each hypothesis should be a specific, testable claim." ), input_schema={ "type": "object", "properties": { "title": { "type": "string", "description": "Short title for the hypothesis.", }, "description": { "type": "string", "description": "Detailed description of what this hypothesis claims.", }, }, "required": ["title", "description"], }, executor=self._add_hypothesis, ) self.register_tool( name="link_phenomenon_to_hypothesis", description=( "Link an existing phenomenon to a hypothesis with a relationship type. " f"Valid relationship types: {', '.join(valid_edge_types)}. " "direct_evidence = the phenomenon IS the hypothesis. " "supports = consistent with the hypothesis. " "prerequisite_met = a necessary condition is satisfied. " "consequence_observed = an expected result of the hypothesis is found. " "contradicts = directly contradicts the hypothesis. " "weakens = makes the hypothesis less likely." ), input_schema={ "type": "object", "properties": { "phenomenon_id": { "type": "string", "description": "ID of the phenomenon (e.g. 'ph-a1b2c3d4').", }, "hypothesis_id": { "type": "string", "description": "ID of the hypothesis (e.g. 'hyp-e5f6g7h8').", }, "edge_type": { "type": "string", "enum": valid_edge_types, "description": "The edge_type of the relationship.", }, "reason": { "type": "string", "description": "The reason this relationship holds (1-2 sentences).", }, }, "required": ["phenomenon_id", "hypothesis_id", "edge_type", "reason"], }, executor=self._link_phenomenon_to_hypothesis, ) async def _add_hypothesis(self, title: str, description: str) -> str: hid = await self.graph.add_hypothesis( title=title, description=description, created_by=self.name, ) return f"Hypothesis created: {hid} — {title} (confidence: 0.50)" async def _link_phenomenon_to_hypothesis( self, phenomenon_id: str, hypothesis_id: str, edge_type: str = "", reason: str = "", # Common LLM misnaming — accept as fallbacks relationship: str = "", note: str = "", ) -> str: edge_type = edge_type or relationship reason = reason or note if not edge_type: return "Error: edge_type is required." try: new_conf = await self.graph.update_hypothesis_confidence( hyp_id=hypothesis_id, phenomenon_id=phenomenon_id, edge_type=edge_type, reason=reason, ) weight = HYPOTHESIS_EDGE_WEIGHTS[edge_type] direction = "+" if weight > 0 else "" return ( f"Linked: {phenomenon_id} —[{edge_type}]→ {hypothesis_id} " f"(weight: {direction}{weight}, new confidence: {new_conf:.3f})" ) except ValueError as e: return f"Error linking: {e}"