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:
Markov 2026-02-22 23:18:59 +01:00
parent 5417980b76
commit b096096f93
3 changed files with 148 additions and 130 deletions

View File

@ -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

View File

@ -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()

View File

@ -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()