actor.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. """Actor LLM: simulates a user driving an agent session."""
  2. from __future__ import annotations
  3. from dataclasses import dataclass
  4. from pathlib import Path
  5. from typing import Any
  6. import anthropic
  7. from jinja2 import Template
  8. ACTOR_TOOL: dict[str, Any] = {
  9. "name": "terminal_action",
  10. "description": "Send an action to the terminal session.",
  11. "input_schema": {
  12. "type": "object",
  13. "properties": {
  14. "action": {
  15. "type": "string",
  16. "enum": ["type", "done", "stuck", "key"],
  17. "description": "The action to take.",
  18. },
  19. "text": {
  20. "type": "string",
  21. "description": "Text to type (only for 'type' action).",
  22. },
  23. "key": {
  24. "type": "string",
  25. "description": "Special key to send (only for 'key' action, e.g., 'ctrl-c').",
  26. },
  27. },
  28. "required": ["action"],
  29. },
  30. }
  31. @dataclass
  32. class ActorAction:
  33. action: str
  34. text: str | None = None
  35. key: str | None = None
  36. @classmethod
  37. def from_tool_result(cls, data: dict[str, Any]) -> ActorAction:
  38. return cls(action=data["action"], text=data.get("text"), key=data.get("key"))
  39. class Actor:
  40. def __init__(self, model: str = "claude-sonnet-4-6", temperature: float = 0.7) -> None:
  41. self.model = model
  42. self.temperature = temperature
  43. self.captures: list[str] = []
  44. self._system_prompt: str = ""
  45. self._client: anthropic.Anthropic = anthropic.Anthropic()
  46. def build_system_prompt(self, posture: str, intents: list[str]) -> str:
  47. template_path = Path(__file__).parent.parent / "prompts" / "actor.md"
  48. template = Template(template_path.read_text())
  49. self._system_prompt = template.render(posture=posture, intents=intents)
  50. return self._system_prompt
  51. def append_capture(self, terminal_output: str) -> None:
  52. self.captures.append(terminal_output)
  53. def build_messages(self) -> list[dict[str, str]]:
  54. return [{"role": "user", "content": capture} for capture in self.captures]
  55. def decide(self) -> ActorAction:
  56. response = self._client.messages.create(
  57. model=self.model,
  58. max_tokens=1024,
  59. temperature=self.temperature,
  60. system=self._system_prompt,
  61. tools=[ACTOR_TOOL], # ty: ignore[invalid-argument-type]
  62. tool_choice={"type": "tool", "name": "terminal_action"},
  63. messages=self.build_messages(), # ty: ignore[invalid-argument-type]
  64. )
  65. for block in response.content:
  66. if block.type == "tool_use":
  67. return ActorAction.from_tool_result(block.input)
  68. raise RuntimeError("Actor did not return a tool_use block")