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
20
README.md
20
README.md
@ -41,22 +41,24 @@ pip install -e .
|
|||||||
3. **Use the agent**:
|
3. **Use the agent**:
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import asyncio
|
||||||
from picogent import Agent, Config
|
from picogent import run_agent, Config
|
||||||
|
|
||||||
# Load configuration
|
|
||||||
config = Config.from_file("config.json")
|
|
||||||
|
|
||||||
# Create agent
|
|
||||||
agent = Agent(config)
|
|
||||||
|
|
||||||
# Run a task
|
|
||||||
async def main():
|
async def main():
|
||||||
response = await agent.chat("Create a hello world Python script")
|
# Load configuration
|
||||||
|
config = Config.from_file("config.json")
|
||||||
|
|
||||||
|
# Run agent with prompt
|
||||||
|
response = await run_agent("List all files in the current directory", config)
|
||||||
print(response)
|
print(response)
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or use the CLI:
|
||||||
|
```bash
|
||||||
|
python -m picogent.cli "Create a hello world Python script"
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Main agent loop with ReAct pattern
|
Main agent with ReAct loop
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Dict, Any, Optional, List
|
import uuid
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .session import Session
|
from .session import Session
|
||||||
from .context import Context
|
from .context import ContextBuilder
|
||||||
from .providers.anthropic import AnthropicProvider
|
from .providers.anthropic import AnthropicProvider
|
||||||
from .tools.registry import registry
|
from .tools.registry import ToolRegistry
|
||||||
from .tools.read import ReadTool
|
from .tools.read import ReadTool
|
||||||
from .tools.write import WriteTool
|
from .tools.write import WriteTool
|
||||||
from .tools.edit import EditTool
|
from .tools.edit import EditTool
|
||||||
@ -17,136 +18,136 @@ from .tools.bash import BashTool
|
|||||||
|
|
||||||
|
|
||||||
class Agent:
|
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.config = config
|
||||||
self.context = context or Context(config.workspace)
|
self.session = Session()
|
||||||
self.provider = None
|
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
|
# Initialize tools
|
||||||
self._initialize_tools()
|
self.tool_registry = ToolRegistry()
|
||||||
|
self._register_default_tools()
|
||||||
|
|
||||||
def _initialize_tools(self):
|
def _register_default_tools(self):
|
||||||
"""Initialize and register all tools"""
|
"""Register default tools"""
|
||||||
# Register tools with workspace context
|
self.tool_registry.register(ReadTool())
|
||||||
registry.register(ReadTool())
|
self.tool_registry.register(WriteTool())
|
||||||
registry.register(WriteTool())
|
self.tool_registry.register(EditTool())
|
||||||
registry.register(EditTool())
|
self.tool_registry.register(BashTool())
|
||||||
registry.register(BashTool(self.context.get_workspace()))
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def run(self, user_message: str, session_file: Optional[str] = None) -> str:
|
||||||
"""Async context manager entry"""
|
"""Run the agent with ReAct loop"""
|
||||||
self.provider = AnthropicProvider(self.config)
|
# Load session if specified
|
||||||
await self.provider.__aenter__()
|
if session_file:
|
||||||
return self
|
self.session.session_file = session_file
|
||||||
|
self.session.load()
|
||||||
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()
|
|
||||||
|
|
||||||
# Add user message to session
|
# 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
|
# ReAct loop
|
||||||
for iteration in range(self.config.max_iterations):
|
iteration = 0
|
||||||
|
max_iterations = self.config.max_iterations
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
# Get messages and system prompt
|
# Generate response
|
||||||
messages = session.get_anthropic_messages()
|
response = await self.provider.generate_response(
|
||||||
system_prompt = self.context.get_system_prompt()
|
|
||||||
tools = registry.get_tool_definitions()
|
|
||||||
|
|
||||||
# Call LLM
|
|
||||||
response = await self.provider.chat_completion(
|
|
||||||
messages=messages,
|
messages=messages,
|
||||||
|
system_prompt=system_prompt,
|
||||||
tools=tools,
|
tools=tools,
|
||||||
system_prompt=system_prompt
|
max_tokens=self.config.max_tokens
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle errors
|
# Check if there are tool calls
|
||||||
if "error" in response:
|
tool_calls = response.get("tool_calls", [])
|
||||||
error_msg = f"Provider error: {response['error']}"
|
|
||||||
session.add_message("assistant", error_msg)
|
|
||||||
return error_msg
|
|
||||||
|
|
||||||
# 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 not tool_calls:
|
||||||
if content:
|
# No tool calls, return text response
|
||||||
session.add_message("assistant", content)
|
response_text = response.get("content", "").strip()
|
||||||
return content
|
if response_text:
|
||||||
else:
|
self.session.add_message("assistant", response_text)
|
||||||
error_msg = "Empty response from provider"
|
|
||||||
session.add_message("assistant", error_msg)
|
# Save session if file specified
|
||||||
return error_msg
|
if session_file:
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
return response_text or "I apologize, but I didn't generate a response."
|
||||||
|
|
||||||
# Add assistant message with tool calls
|
# Process tool calls
|
||||||
if content:
|
# First, add the assistant message with tool calls
|
||||||
# Add text content if present
|
if response.get("raw_content"):
|
||||||
session.add_message("assistant", 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
|
# Execute each tool call
|
||||||
all_results = []
|
|
||||||
for tool_call in tool_calls:
|
for tool_call in tool_calls:
|
||||||
tool_id = tool_call.get("id") or str(uuid.uuid4())
|
tool_id = tool_call.get("id", str(uuid.uuid4()))
|
||||||
tool_name = tool_call.get("name", "")
|
tool_name = tool_call.get("name")
|
||||||
tool_args = tool_call.get("arguments", {})
|
tool_input = tool_call.get("input", {})
|
||||||
|
|
||||||
# Add tool use to session
|
try:
|
||||||
session.add_tool_use(tool_name, tool_args, tool_id)
|
# Execute the tool
|
||||||
|
result = await self.tool_registry.execute_tool(tool_name, tool_input)
|
||||||
# Execute tool
|
|
||||||
result = await registry.execute_tool(tool_name, tool_args)
|
# Add tool result to session
|
||||||
all_results.append(result)
|
self.session.add_tool_result(tool_id, result)
|
||||||
|
|
||||||
# Add tool result to session
|
except Exception as e:
|
||||||
session.add_tool_result(tool_id, result)
|
error_result = f"Error executing {tool_name}: {str(e)}"
|
||||||
|
self.session.add_tool_result(tool_id, error_result)
|
||||||
|
|
||||||
# Continue loop to process tool results
|
# Continue the loop to get the next response
|
||||||
continue
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Agent error in iteration {iteration + 1}: {str(e)}"
|
error_msg = f"Error during agent execution: {str(e)}"
|
||||||
session.add_message("assistant", error_msg)
|
|
||||||
|
# Save session if file specified
|
||||||
|
if session_file:
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
# Max iterations reached
|
# Max iterations reached
|
||||||
final_msg = f"Reached maximum iterations ({self.config.max_iterations}). Task may be incomplete."
|
final_message = f"Maximum iterations ({max_iterations}) reached. The agent may need more steps to complete the task."
|
||||||
session.add_message("assistant", final_msg)
|
|
||||||
return final_msg
|
|
||||||
|
|
||||||
|
|
||||||
# Convenience function
|
|
||||||
async def run_agent(prompt: str, config: Config, session: Optional[Session] = None) -> str:
|
|
||||||
"""
|
|
||||||
Convenience function to run agent
|
|
||||||
|
|
||||||
Args:
|
|
||||||
prompt: User prompt
|
|
||||||
config: Agent configuration
|
|
||||||
session: Optional session for conversation history
|
|
||||||
|
|
||||||
Returns:
|
# Save session if file specified
|
||||||
Agent response
|
if session_file:
|
||||||
"""
|
self.session.save()
|
||||||
async with Agent(config) as agent:
|
|
||||||
return await agent.run_agent(prompt, session)
|
return final_message
|
||||||
|
|
||||||
|
async def chat(self, user_message: str) -> str:
|
||||||
|
"""Simple chat interface without session persistence"""
|
||||||
|
return await self.run(user_message)
|
||||||
|
|
||||||
|
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,17 +19,21 @@ class Tool(ABC):
|
|||||||
"""Execute the tool with given arguments"""
|
"""Execute the tool with given arguments"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def to_anthropic_format(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert tool to Anthropic API format"""
|
"""Convert tool to dictionary for API"""
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
'name': self.name,
|
||||||
"description": self.description,
|
'description': self.description,
|
||||||
"input_schema": {
|
'input_schema': {
|
||||||
"type": "object",
|
'type': 'object',
|
||||||
"properties": self.parameters.get("properties", {}),
|
'properties': self.parameters.get('properties', {}),
|
||||||
"required": self.parameters.get("required", [])
|
'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:
|
class ToolRegistry:
|
||||||
@ -52,11 +56,22 @@ class ToolRegistry:
|
|||||||
"""Get all registered tools"""
|
"""Get all registered tools"""
|
||||||
return list(self.tools.values())
|
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]]:
|
def get_anthropic_tools(self) -> List[Dict[str, Any]]:
|
||||||
"""Get all tools in Anthropic API format"""
|
"""Get all tools in Anthropic API format (alias)"""
|
||||||
return [tool.to_anthropic_format() for tool in self.tools.values()]
|
return self.get_tool_definitions()
|
||||||
|
|
||||||
async def execute_tool(self, name: str, args: Dict[str, Any]) -> str:
|
async def execute_tool(self, name: str, args: Dict[str, Any]) -> str:
|
||||||
"""Execute a tool by name with arguments"""
|
"""Execute a tool by name with arguments"""
|
||||||
tool = self.get_tool(name)
|
try:
|
||||||
return await tool.execute(args)
|
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