verifier.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. """Verifier LLM: evaluates agent session against criteria."""
  2. from __future__ import annotations
  3. from pathlib import Path
  4. import anthropic
  5. from pydantic import BaseModel
  6. class CriterionResult(BaseModel):
  7. criterion: str
  8. verdict: str
  9. evidence: str
  10. rationale: str
  11. source: str = "judge"
  12. class Verdict(BaseModel):
  13. criteria: list[CriterionResult]
  14. observations: list[str]
  15. summary: str
  16. @property
  17. def score(self) -> str:
  18. passed = sum(1 for c in self.criteria if c.verdict == "pass")
  19. return f"{passed}/{len(self.criteria)}"
  20. @property
  21. def passed(self) -> bool:
  22. return all(c.verdict == "pass" for c in self.criteria)
  23. class Verifier:
  24. MAX_RETRIES = 3
  25. def __init__(self, model: str = "claude-sonnet-4-6", temperature: float = 0.0) -> None:
  26. self.model = model
  27. self.temperature = temperature
  28. self._client: anthropic.Anthropic = anthropic.Anthropic()
  29. def build_system_prompt(self) -> str:
  30. template_path = Path(__file__).parent.parent / "prompts" / "verifier.md"
  31. return template_path.read_text()
  32. def verify(
  33. self,
  34. session_log: str,
  35. filesystem_json: str,
  36. tool_calls_jsonl: str,
  37. criteria: list[str],
  38. ) -> Verdict:
  39. system = self.build_system_prompt()
  40. user_content = (
  41. "## Terminal Session Log\n\n"
  42. f"```\n{session_log}\n```\n\n"
  43. "## Filesystem State\n\n"
  44. f"```json\n{filesystem_json}\n```\n\n"
  45. "## Tool Call Log\n\n"
  46. f"```jsonl\n{tool_calls_jsonl}\n```\n\n"
  47. "## Criteria to Evaluate\n\n" + "\n".join(f"- {c}" for c in criteria)
  48. )
  49. for attempt in range(self.MAX_RETRIES):
  50. response = self._client.messages.create(
  51. model=self.model,
  52. max_tokens=4096,
  53. temperature=self.temperature,
  54. system=system,
  55. messages=[{"role": "user", "content": user_content}],
  56. )
  57. text = response.content[0].text # ty: ignore[unresolved-attribute]
  58. json_str = _extract_json(text)
  59. try:
  60. return Verdict.model_validate_json(json_str)
  61. except Exception:
  62. if attempt == self.MAX_RETRIES - 1:
  63. raise
  64. continue
  65. raise RuntimeError("Verifier failed to return valid JSON")
  66. def _extract_json(text: str) -> str:
  67. if "```json" in text:
  68. start = text.index("```json") + 7
  69. end = text.index("```", start)
  70. return text[start:end].strip()
  71. if "```" in text:
  72. start = text.index("```") + 3
  73. end = text.index("```", start)
  74. return text[start:end].strip()
  75. start = text.index("{")
  76. end = text.rindex("}") + 1
  77. return text[start:end]