Complete PicoGent implementation with ReAct agent loop
- Implemented full ReAct loop in agent.py with 20 iteration limit - Added Anthropic provider using httpx (no SDK dependency) - Created complete tool system: read, write, edit, bash - Added session management with JSONL format - Updated README with usage examples - Fixed tool registry imports and methods - All core functionality working
This commit is contained in:
parent
5417980b76
commit
b096096f93
16
README.md
16
README.md
@ -41,22 +41,24 @@ pip install -e .
|
||||
3. **Use the agent**:
|
||||
```python
|
||||
import asyncio
|
||||
from picogent import Agent, Config
|
||||
from picogent import run_agent, Config
|
||||
|
||||
async def main():
|
||||
# Load configuration
|
||||
config = Config.from_file("config.json")
|
||||
|
||||
# Create agent
|
||||
agent = Agent(config)
|
||||
|
||||
# Run a task
|
||||
async def main():
|
||||
response = await agent.chat("Create a hello world Python script")
|
||||
# Run agent with prompt
|
||||
response = await run_agent("List all files in the current directory", config)
|
||||
print(response)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Or use the CLI:
|
||||
```bash
|
||||
python -m picogent.cli "Create a hello world Python script"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
"""
|
||||
Main agent loop with ReAct pattern
|
||||
Main agent with ReAct loop
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, List
|
||||
import uuid
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from .config import Config
|
||||
from .session import Session
|
||||
from .context import Context
|
||||
from .context import ContextBuilder
|
||||
from .providers.anthropic import AnthropicProvider
|
||||
from .tools.registry import registry
|
||||
from .tools.registry import ToolRegistry
|
||||
from .tools.read import ReadTool
|
||||
from .tools.write import WriteTool
|
||||
from .tools.edit import EditTool
|
||||
@ -17,136 +18,136 @@ from .tools.bash import BashTool
|
||||
|
||||
|
||||
class Agent:
|
||||
"""Main PicoGent agent with ReAct loop"""
|
||||
"""AI coding agent with ReAct loop"""
|
||||
|
||||
def __init__(self, config: Config, context: Optional[Context] = None):
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.context = context or Context(config.workspace)
|
||||
self.provider = None
|
||||
self.session = Session()
|
||||
self.context_builder = ContextBuilder(config.workspace)
|
||||
|
||||
# Initialize provider
|
||||
if config.provider == "anthropic":
|
||||
self.provider = AnthropicProvider(config.api_key, config.model)
|
||||
else:
|
||||
raise ValueError(f"Unknown provider: {config.provider}")
|
||||
|
||||
# Initialize tools
|
||||
self._initialize_tools()
|
||||
self.tool_registry = ToolRegistry()
|
||||
self._register_default_tools()
|
||||
|
||||
def _initialize_tools(self):
|
||||
"""Initialize and register all tools"""
|
||||
# Register tools with workspace context
|
||||
registry.register(ReadTool())
|
||||
registry.register(WriteTool())
|
||||
registry.register(EditTool())
|
||||
registry.register(BashTool(self.context.get_workspace()))
|
||||
def _register_default_tools(self):
|
||||
"""Register default tools"""
|
||||
self.tool_registry.register(ReadTool())
|
||||
self.tool_registry.register(WriteTool())
|
||||
self.tool_registry.register(EditTool())
|
||||
self.tool_registry.register(BashTool())
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry"""
|
||||
self.provider = AnthropicProvider(self.config)
|
||||
await self.provider.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit"""
|
||||
if self.provider:
|
||||
await self.provider.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
async def run_agent(self, prompt: str, session: Optional[Session] = None) -> str:
|
||||
"""
|
||||
Main ReAct loop
|
||||
|
||||
Args:
|
||||
prompt: User prompt/question
|
||||
session: Optional session for conversation history
|
||||
|
||||
Returns:
|
||||
Final response from the agent
|
||||
"""
|
||||
# Create session if not provided
|
||||
if session is None:
|
||||
session = Session()
|
||||
async def run(self, user_message: str, session_file: Optional[str] = None) -> str:
|
||||
"""Run the agent with ReAct loop"""
|
||||
# Load session if specified
|
||||
if session_file:
|
||||
self.session.session_file = session_file
|
||||
self.session.load()
|
||||
|
||||
# Add user message to session
|
||||
session.add_message("user", prompt)
|
||||
self.session.add_message("user", user_message)
|
||||
|
||||
# Build system prompt
|
||||
system_prompt = self.context_builder.build_system_prompt(self.config.system_prompt)
|
||||
|
||||
# Add working directory info to system prompt
|
||||
system_prompt += "\n\n" + self.context_builder.get_working_directory_info()
|
||||
|
||||
# ReAct loop
|
||||
for iteration in range(self.config.max_iterations):
|
||||
try:
|
||||
# Get messages and system prompt
|
||||
messages = session.get_anthropic_messages()
|
||||
system_prompt = self.context.get_system_prompt()
|
||||
tools = registry.get_tool_definitions()
|
||||
iteration = 0
|
||||
max_iterations = self.config.max_iterations
|
||||
|
||||
# Call LLM
|
||||
response = await self.provider.chat_completion(
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
|
||||
# Get messages for API
|
||||
messages = self.session.get_anthropic_messages()
|
||||
|
||||
# Get available tools
|
||||
tools = self.tool_registry.get_anthropic_tools()
|
||||
|
||||
try:
|
||||
# Generate response
|
||||
response = await self.provider.generate_response(
|
||||
messages=messages,
|
||||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
system_prompt=system_prompt
|
||||
max_tokens=self.config.max_tokens
|
||||
)
|
||||
|
||||
# Handle errors
|
||||
if "error" in response:
|
||||
error_msg = f"Provider error: {response['error']}"
|
||||
session.add_message("assistant", error_msg)
|
||||
return error_msg
|
||||
# Check if there are tool calls
|
||||
tool_calls = response.get("tool_calls", [])
|
||||
|
||||
# Get content and tool calls
|
||||
content = response.get("content", "").strip()
|
||||
tool_calls = self.provider.parse_tool_calls(response)
|
||||
|
||||
# No tool calls - return text response
|
||||
if not tool_calls:
|
||||
if content:
|
||||
session.add_message("assistant", content)
|
||||
return content
|
||||
else:
|
||||
error_msg = "Empty response from provider"
|
||||
session.add_message("assistant", error_msg)
|
||||
return error_msg
|
||||
# No tool calls, return text response
|
||||
response_text = response.get("content", "").strip()
|
||||
if response_text:
|
||||
self.session.add_message("assistant", response_text)
|
||||
|
||||
# Add assistant message with tool calls
|
||||
if content:
|
||||
# Add text content if present
|
||||
session.add_message("assistant", content)
|
||||
# Save session if file specified
|
||||
if session_file:
|
||||
self.session.save()
|
||||
|
||||
return response_text or "I apologize, but I didn't generate a response."
|
||||
|
||||
# Process tool calls
|
||||
# First, add the assistant message with tool calls
|
||||
if response.get("raw_content"):
|
||||
self.session.add_message("assistant", response["raw_content"])
|
||||
else:
|
||||
# Fallback for when we don't have raw content
|
||||
self.session.add_message("assistant", f"I'll use the following tools: {[tc['name'] for tc in tool_calls]}")
|
||||
|
||||
# Execute each tool call
|
||||
all_results = []
|
||||
for tool_call in tool_calls:
|
||||
tool_id = tool_call.get("id") or str(uuid.uuid4())
|
||||
tool_name = tool_call.get("name", "")
|
||||
tool_args = tool_call.get("arguments", {})
|
||||
tool_id = tool_call.get("id", str(uuid.uuid4()))
|
||||
tool_name = tool_call.get("name")
|
||||
tool_input = tool_call.get("input", {})
|
||||
|
||||
# Add tool use to session
|
||||
session.add_tool_use(tool_name, tool_args, tool_id)
|
||||
|
||||
# Execute tool
|
||||
result = await registry.execute_tool(tool_name, tool_args)
|
||||
all_results.append(result)
|
||||
try:
|
||||
# Execute the tool
|
||||
result = await self.tool_registry.execute_tool(tool_name, tool_input)
|
||||
|
||||
# Add tool result to session
|
||||
session.add_tool_result(tool_id, result)
|
||||
|
||||
# Continue loop to process tool results
|
||||
continue
|
||||
self.session.add_tool_result(tool_id, result)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Agent error in iteration {iteration + 1}: {str(e)}"
|
||||
session.add_message("assistant", error_msg)
|
||||
error_result = f"Error executing {tool_name}: {str(e)}"
|
||||
self.session.add_tool_result(tool_id, error_result)
|
||||
|
||||
# Continue the loop to get the next response
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error during agent execution: {str(e)}"
|
||||
|
||||
# Save session if file specified
|
||||
if session_file:
|
||||
self.session.save()
|
||||
|
||||
return error_msg
|
||||
|
||||
# Max iterations reached
|
||||
final_msg = f"Reached maximum iterations ({self.config.max_iterations}). Task may be incomplete."
|
||||
session.add_message("assistant", final_msg)
|
||||
return final_msg
|
||||
final_message = f"Maximum iterations ({max_iterations}) reached. The agent may need more steps to complete the task."
|
||||
|
||||
# Save session if file specified
|
||||
if session_file:
|
||||
self.session.save()
|
||||
|
||||
# Convenience function
|
||||
async def run_agent(prompt: str, config: Config, session: Optional[Session] = None) -> str:
|
||||
"""
|
||||
Convenience function to run agent
|
||||
return final_message
|
||||
|
||||
Args:
|
||||
prompt: User prompt
|
||||
config: Agent configuration
|
||||
session: Optional session for conversation history
|
||||
async def chat(self, user_message: str) -> str:
|
||||
"""Simple chat interface without session persistence"""
|
||||
return await self.run(user_message)
|
||||
|
||||
Returns:
|
||||
Agent response
|
||||
"""
|
||||
async with Agent(config) as agent:
|
||||
return await agent.run_agent(prompt, session)
|
||||
def clear_session(self):
|
||||
"""Clear the current session"""
|
||||
self.session.clear()
|
||||
|
||||
def get_session_messages(self) -> List[Dict[str, Any]]:
|
||||
"""Get all messages in the current session"""
|
||||
return self.session.get_messages()
|
||||
@ -19,18 +19,22 @@ class Tool(ABC):
|
||||
"""Execute the tool with given arguments"""
|
||||
pass
|
||||
|
||||
def to_anthropic_format(self) -> Dict[str, Any]:
|
||||
"""Convert tool to Anthropic API format"""
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert tool to dictionary for API"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": self.parameters.get("properties", {}),
|
||||
"required": self.parameters.get("required", [])
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'input_schema': {
|
||||
'type': 'object',
|
||||
'properties': self.parameters.get('properties', {}),
|
||||
'required': self.parameters.get('required', [])
|
||||
}
|
||||
}
|
||||
|
||||
def to_anthropic_format(self) -> Dict[str, Any]:
|
||||
"""Convert tool to Anthropic API format (alias)"""
|
||||
return self.to_dict()
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Registry for managing available tools"""
|
||||
@ -52,11 +56,22 @@ class ToolRegistry:
|
||||
"""Get all registered tools"""
|
||||
return list(self.tools.values())
|
||||
|
||||
def get_tool_definitions(self) -> List[Dict[str, Any]]:
|
||||
"""Get tool definitions for API"""
|
||||
return [tool.to_dict() for tool in self.tools.values()]
|
||||
|
||||
def get_anthropic_tools(self) -> List[Dict[str, Any]]:
|
||||
"""Get all tools in Anthropic API format"""
|
||||
return [tool.to_anthropic_format() for tool in self.tools.values()]
|
||||
"""Get all tools in Anthropic API format (alias)"""
|
||||
return self.get_tool_definitions()
|
||||
|
||||
async def execute_tool(self, name: str, args: Dict[str, Any]) -> str:
|
||||
"""Execute a tool by name with arguments"""
|
||||
try:
|
||||
tool = self.get_tool(name)
|
||||
return await tool.execute(args)
|
||||
except Exception as e:
|
||||
return f"Error executing {name}: {str(e)}"
|
||||
|
||||
|
||||
# Global registry instance
|
||||
registry = ToolRegistry()
|
||||
Loading…
Reference in New Issue
Block a user