Autarch/core/agent.py

414 lines
14 KiB
Python
Raw Normal View History

"""
AUTARCH Agent System
Autonomous agent that uses LLM to accomplish tasks with tools
"""
import re
import json
from typing import Optional, List, Dict, Any, Callable
from dataclasses import dataclass, field
from enum import Enum
from .llm import get_llm, LLM, LLMError
from .tools import get_tool_registry, ToolRegistry
from .banner import Colors
class AgentState(Enum):
"""Agent execution states."""
IDLE = "idle"
THINKING = "thinking"
EXECUTING = "executing"
WAITING_USER = "waiting_user"
COMPLETE = "complete"
ERROR = "error"
@dataclass
class AgentStep:
"""Record of a single agent step."""
thought: str
tool_name: Optional[str] = None
tool_args: Optional[Dict[str, Any]] = None
tool_result: Optional[str] = None
error: Optional[str] = None
@dataclass
class AgentResult:
"""Result of an agent task execution."""
success: bool
summary: str
steps: List[AgentStep] = field(default_factory=list)
error: Optional[str] = None
class Agent:
"""Autonomous agent that uses LLM and tools to accomplish tasks."""
SYSTEM_PROMPT = """You are AUTARCH, an autonomous AI agent created by darkHal and Setec Security Labs.
Your purpose is to accomplish tasks using the tools available to you. You think step by step, use tools to gather information and take actions, then continue until the task is complete.
## How to respond
You MUST respond in the following format for EVERY response:
THOUGHT: [Your reasoning about what to do next]
ACTION: [tool_name]
PARAMS: {"param1": "value1", "param2": "value2"}
OR when the task is complete:
THOUGHT: [Summary of what was accomplished]
ACTION: task_complete
PARAMS: {"summary": "Description of completed work"}
OR when you need user input:
THOUGHT: [Why you need to ask the user]
ACTION: ask_user
PARAMS: {"question": "Your question"}
## Rules
1. Always start with THOUGHT to explain your reasoning
2. Always specify exactly one ACTION
3. Always provide PARAMS as valid JSON (even if empty: {})
4. Use tools to verify your work - don't assume success
5. If a tool fails, analyze the error and try a different approach
6. Only use task_complete when the task is fully done
{tools_description}
"""
def __init__(
self,
llm: LLM = None,
tools: ToolRegistry = None,
max_steps: int = 20,
verbose: bool = True
):
"""Initialize the agent.
Args:
llm: LLM instance to use. Uses global if not provided.
tools: Tool registry to use. Uses global if not provided.
max_steps: Maximum steps before stopping.
verbose: Whether to print progress.
"""
self.llm = llm or get_llm()
self.tools = tools or get_tool_registry()
self.max_steps = max_steps
self.verbose = verbose
self.state = AgentState.IDLE
self.current_task: Optional[str] = None
self.steps: List[AgentStep] = []
self.conversation: List[Dict[str, str]] = []
# Callbacks
self.on_step: Optional[Callable[[AgentStep], None]] = None
self.on_state_change: Optional[Callable[[AgentState], None]] = None
def _set_state(self, state: AgentState):
"""Update agent state and notify callback."""
self.state = state
if self.on_state_change:
self.on_state_change(state)
def _log(self, message: str, level: str = "info"):
"""Log a message if verbose mode is on."""
if not self.verbose:
return
colors = {
"info": Colors.CYAN,
"success": Colors.GREEN,
"warning": Colors.YELLOW,
"error": Colors.RED,
"thought": Colors.MAGENTA,
"action": Colors.BLUE,
"result": Colors.WHITE,
}
symbols = {
"info": "*",
"success": "+",
"warning": "!",
"error": "X",
"thought": "?",
"action": ">",
"result": "<",
}
color = colors.get(level, Colors.WHITE)
symbol = symbols.get(level, "*")
print(f"{color}[{symbol}] {message}{Colors.RESET}")
def _build_system_prompt(self) -> str:
"""Build the system prompt with tools description."""
tools_desc = self.tools.get_tools_prompt()
return self.SYSTEM_PROMPT.format(tools_description=tools_desc)
def _parse_response(self, response: str) -> tuple[str, str, Dict[str, Any]]:
"""Parse LLM response into thought, action, and params.
Args:
response: The raw LLM response.
Returns:
Tuple of (thought, action_name, params_dict)
Raises:
ValueError: If response cannot be parsed.
"""
# Extract THOUGHT
thought_match = re.search(r'THOUGHT:\s*(.+?)(?=ACTION:|$)', response, re.DOTALL)
thought = thought_match.group(1).strip() if thought_match else ""
# Extract ACTION
action_match = re.search(r'ACTION:\s*(\w+)', response)
if not action_match:
raise ValueError("No ACTION found in response")
action = action_match.group(1).strip()
# Extract PARAMS
params_match = re.search(r'PARAMS:\s*(\{.*?\})', response, re.DOTALL)
if params_match:
try:
params = json.loads(params_match.group(1))
except json.JSONDecodeError:
# Try to fix common JSON issues
params_str = params_match.group(1)
# Replace single quotes with double quotes
params_str = params_str.replace("'", '"')
try:
params = json.loads(params_str)
except json.JSONDecodeError:
params = {}
else:
params = {}
return thought, action, params
def _execute_tool(self, tool_name: str, params: Dict[str, Any]) -> str:
"""Execute a tool and return the result.
Args:
tool_name: Name of the tool to execute.
params: Parameters for the tool.
Returns:
Tool result string.
"""
result = self.tools.execute(tool_name, **params)
if result["success"]:
return str(result["result"])
else:
return f"[Error]: {result['error']}"
def run(self, task: str, user_input_handler: Callable[[str], str] = None,
step_callback: Optional[Callable[['AgentStep'], None]] = None) -> AgentResult:
"""Run the agent on a task.
Args:
task: The task description.
user_input_handler: Callback for handling ask_user actions.
If None, uses default input().
step_callback: Optional per-step callback invoked after each step completes.
Overrides self.on_step for this run if provided.
Returns:
AgentResult with execution details.
"""
if step_callback is not None:
self.on_step = step_callback
self.current_task = task
self.steps = []
self.conversation = []
# Ensure model is loaded
if not self.llm.is_loaded:
self._log("Loading model...", "info")
try:
self.llm.load_model(verbose=self.verbose)
except LLMError as e:
self._set_state(AgentState.ERROR)
return AgentResult(
success=False,
summary="Failed to load model",
error=str(e)
)
self._set_state(AgentState.THINKING)
self._log(f"Starting task: {task}", "info")
# Build initial prompt
system_prompt = self._build_system_prompt()
self.conversation.append({"role": "system", "content": system_prompt})
self.conversation.append({"role": "user", "content": f"Task: {task}"})
step_count = 0
while step_count < self.max_steps:
step_count += 1
self._log(f"Step {step_count}/{self.max_steps}", "info")
# Generate response
self._set_state(AgentState.THINKING)
try:
prompt = self._build_prompt()
response = self.llm.generate(
prompt,
stop=["OBSERVATION:", "\nUser:", "\nTask:"],
temperature=0.3, # Lower temperature for more focused responses
)
except LLMError as e:
self._set_state(AgentState.ERROR)
return AgentResult(
success=False,
summary="LLM generation failed",
steps=self.steps,
error=str(e)
)
# Parse response
try:
thought, action, params = self._parse_response(response)
except ValueError as e:
self._log(f"Failed to parse response: {e}", "error")
self._log(f"Raw response: {response[:200]}...", "warning")
# Add error feedback and continue
self.conversation.append({
"role": "assistant",
"content": response
})
self.conversation.append({
"role": "user",
"content": "Error: Could not parse your response. Please use the exact format:\nTHOUGHT: [reasoning]\nACTION: [tool_name]\nPARAMS: {\"param\": \"value\"}"
})
continue
self._log(f"Thought: {thought[:100]}..." if len(thought) > 100 else f"Thought: {thought}", "thought")
self._log(f"Action: {action}", "action")
step = AgentStep(thought=thought, tool_name=action, tool_args=params)
# Handle task_complete
if action == "task_complete":
summary = params.get("summary", thought)
step.tool_result = summary
self.steps.append(step)
if self.on_step:
self.on_step(step)
self._set_state(AgentState.COMPLETE)
self._log(f"Task complete: {summary}", "success")
return AgentResult(
success=True,
summary=summary,
steps=self.steps
)
# Handle ask_user
if action == "ask_user":
question = params.get("question", "What should I do?")
self._set_state(AgentState.WAITING_USER)
self._log(f"Agent asks: {question}", "info")
if user_input_handler:
user_response = user_input_handler(question)
else:
print(f"\n{Colors.YELLOW}Agent question: {question}{Colors.RESET}")
user_response = input(f"{Colors.GREEN}Your answer: {Colors.RESET}").strip()
step.tool_result = f"User response: {user_response}"
self.steps.append(step)
if self.on_step:
self.on_step(step)
# Add to conversation
self.conversation.append({
"role": "assistant",
"content": f"THOUGHT: {thought}\nACTION: {action}\nPARAMS: {json.dumps(params)}"
})
self.conversation.append({
"role": "user",
"content": f"OBSERVATION: User responded: {user_response}"
})
continue
# Execute tool
self._set_state(AgentState.EXECUTING)
self._log(f"Executing: {action}({params})", "action")
result = self._execute_tool(action, params)
step.tool_result = result
self.steps.append(step)
if self.on_step:
self.on_step(step)
# Truncate long results for display
display_result = result[:200] + "..." if len(result) > 200 else result
self._log(f"Result: {display_result}", "result")
# Add to conversation
self.conversation.append({
"role": "assistant",
"content": f"THOUGHT: {thought}\nACTION: {action}\nPARAMS: {json.dumps(params)}"
})
self.conversation.append({
"role": "user",
"content": f"OBSERVATION: {result}"
})
# Max steps reached
self._set_state(AgentState.ERROR)
self._log(f"Max steps ({self.max_steps}) reached", "warning")
return AgentResult(
success=False,
summary="Max steps reached without completing task",
steps=self.steps,
error=f"Exceeded maximum of {self.max_steps} steps"
)
def _build_prompt(self) -> str:
"""Build the full prompt from conversation history."""
parts = []
for msg in self.conversation:
role = msg["role"]
content = msg["content"]
if role == "system":
parts.append(f"<|im_start|>system\n{content}<|im_end|>")
elif role == "user":
parts.append(f"<|im_start|>user\n{content}<|im_end|>")
elif role == "assistant":
parts.append(f"<|im_start|>assistant\n{content}<|im_end|>")
parts.append("<|im_start|>assistant\n")
return "\n".join(parts)
def get_steps_summary(self) -> str:
"""Get a formatted summary of all steps taken."""
if not self.steps:
return "No steps executed"
lines = []
for i, step in enumerate(self.steps, 1):
lines.append(f"Step {i}:")
lines.append(f" Thought: {step.thought[:80]}...")
if step.tool_name:
lines.append(f" Action: {step.tool_name}")
if step.tool_result:
result_preview = step.tool_result[:80] + "..." if len(step.tool_result) > 80 else step.tool_result
lines.append(f" Result: {result_preview}")
lines.append("")
return "\n".join(lines)