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**:
```python
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():
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)
asyncio.run(main())
```
Or use the CLI:
```bash
python -m picogent.cli "Create a hello world Python script"
```
## Architecture
### Core Components

View File

@ -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)
try:
# 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)
all_results.append(result)
# Add tool result to session
self.session.add_tool_result(tool_id, result)
# Add tool result to session
session.add_tool_result(tool_id, result)
except Exception as e:
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
# Continue the loop to get the next response
except Exception as e:
error_msg = f"Agent error in iteration {iteration + 1}: {str(e)}"
session.add_message("assistant", error_msg)
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()

View File

@ -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"""
tool = self.get_tool(name)
return await tool.execute(args)
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()