why-agent / agent /state.py
MapoTofu9's picture
deploy: HF Spaces
5d30bdc
Raw
History Blame Contribute Delete
4.33 kB
"""Pydantic state model for the why-agent LangGraph state machine."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Phase enum β€” the ordered steps of the investigation loop
# ---------------------------------------------------------------------------
class Phase(StrEnum):
PLAN = "plan"
DECOMPOSE = "decompose"
DRILL = "drill"
CROSS_CHECK = "cross_check"
CRITIQUE = "critique"
REPORT = "report"
# ---------------------------------------------------------------------------
# Evidence record β€” one entry per tool call
# ---------------------------------------------------------------------------
class EvidenceEntry(BaseModel):
phase: Phase
tool_name: str
args: dict[str, Any]
output: dict[str, Any]
timestamp: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
reasoning: str | None = Field(
default=None, description="LLM reasoning text that preceded this tool call."
)
duration_ms: float | None = Field(
default=None, description="Tool execution time in milliseconds."
)
# ---------------------------------------------------------------------------
# Hypothesis β€” one candidate explanation under active investigation
# ---------------------------------------------------------------------------
class Hypothesis(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8].upper())
description: str
supporting_evidence: list[str] = Field(default_factory=list)
weakening_evidence: list[str] = Field(default_factory=list)
status: str = "active"
# ---------------------------------------------------------------------------
# Tool-call result β€” carried between LLM node and tool-executor node
# ---------------------------------------------------------------------------
class ToolResult(BaseModel):
tool_name: str
args: dict[str, Any]
output: dict[str, Any] = Field(default_factory=dict)
# ---------------------------------------------------------------------------
# Full graph state
# ---------------------------------------------------------------------------
class InvestigationState(BaseModel):
user_question: str = Field(
description="Original user question, e.g. 'Why did PR activity drop on Oct 21 2018?'"
)
phase: Phase = Field(default=Phase.PLAN)
evidence: list[EvidenceEntry] = Field(
default_factory=list,
description="Append-only log of every tool call made during this investigation.",
)
hypotheses: list[Hypothesis] = Field(
default_factory=list,
description="All hypotheses raised so far.",
)
active_hypothesis_id: str | None = Field(
default=None,
description="Which hypothesis the agent is currently drilling into.",
)
pending_tool_calls: list[ToolResult] = Field(
default_factory=list,
description="Tool calls returned by the LLM that have not been executed yet.",
)
messages: list[Any] = Field(default_factory=list)
pending_reasoning: str | None = Field(
default=None,
description="LLM text from the most recent llm_call, attached to the first tool entry of the next batch.",
)
question_type: Literal["CROSS_SECTIONAL", "TIME_SERIES", "EXPLORATORY"] | None = Field(
default=None,
description="Question classification set during plan phase.",
)
critique_feedback: str | None = Field(
default=None,
description="Explanation from the last VERDICT: weak critique β€” injected into the next phase as a targeted directive.",
)
critique_passed: bool = Field(
default=False,
description="Set True by critique node when evidence is strong enough to report.",
)
retry_count: int = Field(default=0, ge=0)
final_report: dict[str, Any] | None = Field(default=None)
error: str | None = Field(default=None)
def add_evidence(self, entry: EvidenceEntry) -> None:
self.evidence.append(entry)
def next_hypothesis_id(self) -> str:
n = len(self.hypotheses) + 1
return f"H{n}"