mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-30 21:49:30 -05:00
feat: add agent work orders microservice with hybrid deployment
This commit is contained in:
168
python/src/agent_work_orders/CLAUDE.md
Normal file
168
python/src/agent_work_orders/CLAUDE.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# AI Agent Development Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
agent_work_orders for claude code cli automation stichting modular workflows together
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **TYPE SAFETY IS NON-NEGOTIABLE**
|
||||
- All functions, methods, and variables MUST have type annotations
|
||||
- Strict mypy configuration is enforced
|
||||
- No `Any` types without explicit justification
|
||||
|
||||
2. **KISS** (Keep It Simple, Stupid)
|
||||
- Prefer simple, readable solutions over clever abstractions
|
||||
|
||||
3. **YAGNI** (You Aren't Gonna Need It)
|
||||
- Don't build features until they're actually needed
|
||||
|
||||
**Architecture:**
|
||||
|
||||
```
|
||||
src/agent_work_orders
|
||||
```
|
||||
|
||||
Each tool is a vertical slice containing tool.py, schemas.py, service.py.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Style
|
||||
|
||||
**Use Google-style docstrings** for all functions, classes, and modules:
|
||||
|
||||
```python
|
||||
def process_request(user_id: str, query: str) -> dict[str, Any]:
|
||||
"""Process a user request and return results.
|
||||
|
||||
Args:
|
||||
user_id: Unique identifier for the user.
|
||||
query: The search query string.
|
||||
|
||||
Returns:
|
||||
Dictionary containing results and metadata.
|
||||
|
||||
Raises:
|
||||
ValueError: If query is empty or invalid.
|
||||
ProcessingError: If processing fails after retries.
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging Rules
|
||||
|
||||
**Philosophy:** Logs are optimized for AI agent consumption. Include enough context for an LLM to understand and fix issues without human intervention.
|
||||
|
||||
### Required (MUST)
|
||||
|
||||
1. **Import shared logger:** from python/src/agent_work_orders/utils/structured_logger.py
|
||||
|
||||
2. **Use appropriate levels:** `debug` (diagnostics), `info` (operations), `warning` (recoverable), `error` (non-fatal), `exception` (in except blocks with stack traces)
|
||||
|
||||
3. **Use structured logging:** Always use keyword arguments, never string formatting
|
||||
|
||||
```python
|
||||
logger.info("user_created", user_id="123", role="admin") # ✅
|
||||
logger.info(f"User {user_id} created") # ❌ NO
|
||||
```
|
||||
|
||||
4. **Descriptive event names:** Use `snake_case` that answers "what happened?"
|
||||
- Good: `database_connection_established`, `tool_execution_started`, `api_request_completed`
|
||||
- Bad: `connected`, `done`, `success`
|
||||
|
||||
5. **Use logger.exception() in except blocks:** Captures full stack trace automatically
|
||||
|
||||
```python
|
||||
try:
|
||||
result = await operation()
|
||||
except ValueError:
|
||||
logger.exception("operation_failed", expected="int", received=type(value).__name__)
|
||||
raise
|
||||
```
|
||||
|
||||
6. **Include debugging context:** IDs (user_id, request_id, session_id), input values, expected vs actual, external responses, performance metrics (duration_ms)
|
||||
|
||||
### Recommended (SHOULD)
|
||||
|
||||
- Log entry/exit for complex operations with relevant metadata
|
||||
- Log performance metrics for bottlenecks (timing, counts)
|
||||
- Log state transitions (old_state, new_state)
|
||||
- Log external system interactions (API calls, database queries, tool executions)
|
||||
|
||||
### DO NOT
|
||||
|
||||
- **DO NOT log sensitive data:** No passwords, API keys, tokens (mask: `api_key[:8] + "..."`)
|
||||
- **DO NOT use string formatting:** Always use structured kwargs
|
||||
- **DO NOT spam logs in loops:** Log batch summaries instead
|
||||
- **DO NOT silently catch exceptions:** Always log with `logger.exception()` or re-raise
|
||||
- **DO NOT use vague event names:** Be specific about what happened
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Tool execution:**
|
||||
|
||||
```python
|
||||
logger.info("tool_execution_started", tool=name, params=params)
|
||||
try:
|
||||
result = await tool.execute(params)
|
||||
logger.info("tool_execution_completed", tool=name, duration_ms=duration)
|
||||
except ToolError:
|
||||
logger.exception("tool_execution_failed", tool=name, retry_count=count)
|
||||
raise
|
||||
```
|
||||
|
||||
**External API calls:**
|
||||
|
||||
```python
|
||||
logger.info("api_call", provider="openai", endpoint="/v1/chat", status=200,
|
||||
duration_ms=1245.5, tokens={"prompt": 245, "completion": 128})
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Logs include: `correlation_id` (links request logs), `source` (file:function:line), `duration_ms` (performance), `exc_type/exc_message` (errors). Use `grep "correlation_id=abc-123"` to trace requests.
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
**Run server:** `uv run uvicorn src.main:app --host 0.0.0.0 --port 8030 --reload`
|
||||
|
||||
**Lint/check (must pass):** `uv run ruff check src/ && uv run mypy src/`
|
||||
|
||||
**Auto-fix:** `uv run ruff check --fix src/`
|
||||
|
||||
**Run tests:** `uv run pytest tests/ -v`
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
**Tests mirror the source directory structure.** Every file in `src/agent_work_orders` MUST have a corresponding test file.
|
||||
|
||||
**Structure:**
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- **Unit tests:** Test individual components in isolation. Mark with `@pytest.mark.unit`
|
||||
- **Integration tests:** Test multiple components together. Mark with `@pytest.mark.integration`
|
||||
- Place integration tests in `tests/integration/` when testing full application stack
|
||||
|
||||
**Run tests:** `uv run pytest tests/ -v`
|
||||
|
||||
**Run specific types:** `uv run pytest tests/ -m unit` or `uv run pytest tests/ -m integration`
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## AI Agent Notes
|
||||
|
||||
When debugging:
|
||||
|
||||
- Check `source` field for file/function location
|
||||
- Use `correlation_id` to trace full request flow
|
||||
- Look for `duration_ms` to identify bottlenecks
|
||||
- Exception logs include full stack traces with local variables (dev mode)
|
||||
- All context is in structured log fields—use them to understand and fix issues
|
||||
316
python/src/agent_work_orders/README.md
Normal file
316
python/src/agent_work_orders/README.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# Agent Work Orders Service
|
||||
|
||||
Independent microservice for executing agent-based workflows using Claude Code CLI.
|
||||
|
||||
## Purpose
|
||||
|
||||
The Agent Work Orders service is a standalone FastAPI application that:
|
||||
|
||||
- Executes Claude Code CLI commands for automated development workflows
|
||||
- Manages git worktrees for isolated execution environments
|
||||
- Integrates with GitHub for PR creation and management
|
||||
- Provides a complete workflow orchestration system with 6 compositional commands
|
||||
|
||||
## Architecture
|
||||
|
||||
This service runs independently from the main Archon server and can be deployed:
|
||||
|
||||
- **Locally**: For development using `uv run`
|
||||
- **Docker**: As a standalone container
|
||||
- **Hybrid**: Mix of local and Docker services
|
||||
|
||||
### Service Communication
|
||||
|
||||
The agent service communicates with:
|
||||
|
||||
- **Archon Server** (`http://archon-server:8181` or `http://localhost:8181`)
|
||||
- **Archon MCP** (`http://archon-mcp:8051` or `http://localhost:8051`)
|
||||
|
||||
Service discovery is automatic based on `SERVICE_DISCOVERY_MODE`:
|
||||
|
||||
- `local`: Uses localhost URLs
|
||||
- `docker_compose`: Uses Docker container names
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.12+
|
||||
- Claude Code CLI installed (`curl -fsSL https://claude.ai/install.sh | bash`)
|
||||
- Git and GitHub CLI (`gh`)
|
||||
- uv package manager
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Using make (recommended)
|
||||
make agent-work-orders
|
||||
|
||||
# Or using the provided script
|
||||
cd python
|
||||
./scripts/start-agent-service.sh
|
||||
|
||||
# Or manually
|
||||
export SERVICE_DISCOVERY_MODE=local
|
||||
export ARCHON_SERVER_URL=http://localhost:8181
|
||||
export ARCHON_MCP_URL=http://localhost:8051
|
||||
uv run python -m uvicorn src.agent_work_orders.server:app --port 8053 --reload
|
||||
```
|
||||
|
||||
## Running with Docker
|
||||
|
||||
### Build and Run
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
cd python
|
||||
docker build -f Dockerfile.agent-work-orders -t archon-agent-work-orders .
|
||||
|
||||
# Run the container
|
||||
docker run -p 8053:8053 \
|
||||
-e SERVICE_DISCOVERY_MODE=local \
|
||||
-e ARCHON_SERVER_URL=http://localhost:8181 \
|
||||
archon-agent-work-orders
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
# Start with agent work orders service profile
|
||||
docker compose --profile work-orders up -d
|
||||
|
||||
# Or include in default services (edit docker-compose.yml to remove profile)
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `AGENT_WORK_ORDERS_PORT` | `8053` | Port for agent work orders service |
|
||||
| `SERVICE_DISCOVERY_MODE` | `local` | Service discovery mode (`local` or `docker_compose`) |
|
||||
| `ARCHON_SERVER_URL` | Auto | Main server URL (auto-configured by discovery mode) |
|
||||
| `ARCHON_MCP_URL` | Auto | MCP server URL (auto-configured by discovery mode) |
|
||||
| `CLAUDE_CLI_PATH` | `claude` | Path to Claude CLI executable |
|
||||
| `GH_CLI_PATH` | `gh` | Path to GitHub CLI executable |
|
||||
| `LOG_LEVEL` | `INFO` | Logging level |
|
||||
| `STATE_STORAGE_TYPE` | `memory` | State storage (`memory` or `file`) - Use `file` for persistence |
|
||||
| `FILE_STATE_DIRECTORY` | `agent-work-orders-state` | Directory for file-based state (when `STATE_STORAGE_TYPE=file`) |
|
||||
|
||||
### Service Discovery Modes
|
||||
|
||||
**Local Mode** (`SERVICE_DISCOVERY_MODE=local`):
|
||||
- Default for development
|
||||
- Services on `localhost` with different ports
|
||||
- Ideal for mixed local/Docker setup
|
||||
|
||||
**Docker Compose Mode** (`SERVICE_DISCOVERY_MODE=docker_compose`):
|
||||
- Automatic in Docker deployments
|
||||
- Uses container names for service discovery
|
||||
- All services in same Docker network
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Core Endpoints
|
||||
|
||||
- `GET /health` - Health check with dependency validation
|
||||
- `GET /` - Service information
|
||||
- `GET /docs` - OpenAPI documentation
|
||||
|
||||
### Work Order Endpoints
|
||||
|
||||
All endpoints under `/api/agent-work-orders`:
|
||||
|
||||
- `POST /` - Create new work order
|
||||
- `GET /` - List all work orders (optional status filter)
|
||||
- `GET /{id}` - Get specific work order
|
||||
- `GET /{id}/steps` - Get step execution history
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Hybrid (Recommended - Backend in Docker, Agent Work Orders Local)
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start backend in Docker and frontend
|
||||
make dev-work-orders
|
||||
|
||||
# Terminal 2: Start agent work orders service
|
||||
make agent-work-orders
|
||||
```
|
||||
|
||||
### All Local (3 terminals)
|
||||
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
cd python
|
||||
uv run python -m uvicorn src.server.main:app --port 8181 --reload
|
||||
|
||||
# Terminal 2: Agent Work Orders Service
|
||||
make agent-work-orders
|
||||
|
||||
# Terminal 3: Frontend
|
||||
cd archon-ui-main
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Full Docker
|
||||
|
||||
```bash
|
||||
# All services in Docker
|
||||
docker compose --profile work-orders up -d
|
||||
|
||||
# View agent work orders service logs
|
||||
docker compose logs -f archon-agent-work-orders
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Claude CLI Not Found
|
||||
|
||||
```bash
|
||||
# Install Claude Code CLI
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
# Verify installation
|
||||
claude --version
|
||||
```
|
||||
|
||||
### Service Connection Errors
|
||||
|
||||
Check health endpoint to see dependency status:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8052/health
|
||||
```
|
||||
|
||||
This shows:
|
||||
- Claude CLI availability
|
||||
- Git availability
|
||||
- Archon server connectivity
|
||||
- MCP server connectivity
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
If port 8053 is in use:
|
||||
|
||||
```bash
|
||||
# Change port
|
||||
export AGENT_WORK_ORDERS_PORT=9053
|
||||
./scripts/start-agent-service.sh
|
||||
```
|
||||
|
||||
### Docker Service Discovery
|
||||
|
||||
If services can't reach each other in Docker:
|
||||
|
||||
```bash
|
||||
# Verify network
|
||||
docker network inspect archon_app-network
|
||||
|
||||
# Test connectivity
|
||||
docker exec archon-agent-work-orders ping archon-server
|
||||
docker exec archon-agent-work-orders curl http://archon-server:8181/health
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run pytest tests/agent_work_orders/ -m unit -v
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```bash
|
||||
uv run pytest tests/integration/test_agent_service_communication.py -v
|
||||
```
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```bash
|
||||
# Create a work order
|
||||
curl -X POST http://localhost:8053/api/agent-work-orders/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"repository_url": "https://github.com/test/repo",
|
||||
"sandbox_type": "worktree",
|
||||
"user_request": "Fix authentication bug",
|
||||
"selected_commands": ["create-branch", "planning"]
|
||||
}'
|
||||
|
||||
# List work orders
|
||||
curl http://localhost:8053/api/agent-work-orders/
|
||||
|
||||
# Get specific work order
|
||||
curl http://localhost:8053/api/agent-work-orders/<id>
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
The `/health` endpoint provides detailed status:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"service": "agent-work-orders",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"claude_cli": { "available": true, "version": "2.0.21" },
|
||||
"git": { "available": true },
|
||||
"archon_server": { "available": true, "url": "..." },
|
||||
"archon_mcp": { "available": true, "url": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
Structured logging with context:
|
||||
|
||||
```bash
|
||||
# Docker logs
|
||||
docker compose logs -f archon-agent-work-orders
|
||||
|
||||
# Local logs (stdout)
|
||||
# Already visible in terminal running the service
|
||||
```
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **FastAPI**: Web framework
|
||||
- **httpx**: HTTP client for service communication
|
||||
- **Claude Code CLI**: Agent execution
|
||||
- **Git**: Repository operations
|
||||
- **GitHub CLI**: PR management
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/agent_work_orders/
|
||||
├── server.py # Standalone server entry point
|
||||
├── main.py # Legacy FastAPI app (deprecated)
|
||||
├── config.py # Configuration management
|
||||
├── api/
|
||||
│ └── routes.py # API route handlers
|
||||
├── agent_executor/ # Claude CLI execution
|
||||
├── workflow_engine/ # Workflow orchestration
|
||||
├── sandbox_manager/ # Git worktree management
|
||||
└── github_integration/ # GitHub operations
|
||||
```
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- Claude Agent SDK migration (replace CLI with Python SDK)
|
||||
- Direct MCP tool integration
|
||||
- Multiple instance scaling with load balancing
|
||||
- Prometheus metrics and distributed tracing
|
||||
- WebSocket support for real-time log streaming
|
||||
- Queue system (RabbitMQ/Redis) for work order management
|
||||
@@ -317,16 +317,25 @@ async def get_agent_work_order_steps(agent_work_order_id: str) -> StepHistory:
|
||||
|
||||
Returns detailed history of each step executed,
|
||||
including success/failure, duration, and errors.
|
||||
Returns empty history if work order exists but has no steps yet.
|
||||
"""
|
||||
logger.info("agent_step_history_get_started", agent_work_order_id=agent_work_order_id)
|
||||
|
||||
try:
|
||||
# First check if work order exists
|
||||
result = await state_repository.get(agent_work_order_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Work order not found")
|
||||
|
||||
step_history = await state_repository.get_step_history(agent_work_order_id)
|
||||
|
||||
if not step_history:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Step history not found for work order {agent_work_order_id}"
|
||||
# Work order exists but no steps yet - return empty history
|
||||
logger.info(
|
||||
"agent_step_history_empty",
|
||||
agent_work_order_id=agent_work_order_id,
|
||||
)
|
||||
return StepHistory(agent_work_order_id=agent_work_order_id, steps=[])
|
||||
|
||||
logger.info(
|
||||
"agent_step_history_get_completed",
|
||||
|
||||
@@ -29,6 +29,12 @@ class AgentWorkOrdersConfig:
|
||||
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
||||
GH_CLI_PATH: str = os.getenv("GH_CLI_PATH", "gh")
|
||||
|
||||
# Service discovery configuration
|
||||
SERVICE_DISCOVERY_MODE: str = os.getenv("SERVICE_DISCOVERY_MODE", "local")
|
||||
|
||||
# CORS configuration
|
||||
CORS_ORIGINS: str = os.getenv("CORS_ORIGINS", "http://localhost:3737,http://host.docker.internal:3737,*")
|
||||
|
||||
# Claude CLI flags configuration
|
||||
# --verbose: Required when using --print with --output-format=stream-json
|
||||
CLAUDE_CLI_VERBOSE: bool = os.getenv("CLAUDE_CLI_VERBOSE", "true").lower() == "true"
|
||||
@@ -69,6 +75,32 @@ class AgentWorkOrdersConfig:
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
return temp_dir
|
||||
|
||||
@classmethod
|
||||
def get_archon_server_url(cls) -> str:
|
||||
"""Get Archon server URL based on service discovery mode"""
|
||||
# Allow explicit override
|
||||
explicit_url = os.getenv("ARCHON_SERVER_URL")
|
||||
if explicit_url:
|
||||
return explicit_url
|
||||
|
||||
# Otherwise use service discovery mode
|
||||
if cls.SERVICE_DISCOVERY_MODE == "docker_compose":
|
||||
return "http://archon-server:8181"
|
||||
return "http://localhost:8181"
|
||||
|
||||
@classmethod
|
||||
def get_archon_mcp_url(cls) -> str:
|
||||
"""Get Archon MCP server URL based on service discovery mode"""
|
||||
# Allow explicit override
|
||||
explicit_url = os.getenv("ARCHON_MCP_URL")
|
||||
if explicit_url:
|
||||
return explicit_url
|
||||
|
||||
# Otherwise use service discovery mode
|
||||
if cls.SERVICE_DISCOVERY_MODE == "docker_compose":
|
||||
return "http://archon-mcp:8051"
|
||||
return "http://localhost:8051"
|
||||
|
||||
|
||||
# Global config instance
|
||||
config = AgentWorkOrdersConfig()
|
||||
|
||||
214
python/src/agent_work_orders/server.py
Normal file
214
python/src/agent_work_orders/server.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Standalone Server Entry Point
|
||||
|
||||
FastAPI server for independent agent work order service.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .api.routes import router
|
||||
from .config import config
|
||||
from .utils.structured_logger import configure_structured_logging, get_logger
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Lifespan context manager for startup and shutdown tasks"""
|
||||
logger = get_logger(__name__)
|
||||
|
||||
logger.info(
|
||||
"Starting Agent Work Orders service",
|
||||
extra={
|
||||
"port": os.getenv("AGENT_WORK_ORDERS_PORT", "8053"),
|
||||
"service_discovery_mode": os.getenv("SERVICE_DISCOVERY_MODE", "local"),
|
||||
},
|
||||
)
|
||||
|
||||
# Validate Claude CLI is available
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[config.CLAUDE_CLI_PATH, "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.info(
|
||||
"Claude CLI validation successful",
|
||||
extra={"version": result.stdout.strip()},
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"Claude CLI validation failed",
|
||||
extra={"error": result.stderr},
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
"Claude CLI not found",
|
||||
extra={"path": config.CLAUDE_CLI_PATH},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Claude CLI validation error",
|
||||
extra={"error": str(e)},
|
||||
)
|
||||
|
||||
# Validate git is available
|
||||
if not shutil.which("git"):
|
||||
logger.error("Git not found in PATH")
|
||||
else:
|
||||
logger.info("Git validation successful")
|
||||
|
||||
# Log service URLs
|
||||
archon_server_url = os.getenv("ARCHON_SERVER_URL")
|
||||
archon_mcp_url = os.getenv("ARCHON_MCP_URL")
|
||||
|
||||
if archon_server_url:
|
||||
logger.info(
|
||||
"Service discovery configured",
|
||||
extra={
|
||||
"archon_server_url": archon_server_url,
|
||||
"archon_mcp_url": archon_mcp_url,
|
||||
},
|
||||
)
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Shutting down Agent Work Orders service")
|
||||
|
||||
|
||||
# Configure logging on startup
|
||||
configure_structured_logging(config.LOG_LEVEL)
|
||||
|
||||
# Create FastAPI app with lifespan
|
||||
app = FastAPI(
|
||||
title="Agent Work Orders API",
|
||||
description="Independent agent work order service for workflow-based agent execution",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS middleware with permissive settings for development
|
||||
cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routes with /api/agent-work-orders prefix
|
||||
app.include_router(router, prefix="/api/agent-work-orders")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> dict[str, Any]:
|
||||
"""Health check endpoint with dependency validation"""
|
||||
health_status: dict[str, Any] = {
|
||||
"status": "healthy",
|
||||
"service": "agent-work-orders",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {},
|
||||
}
|
||||
|
||||
# Check Claude CLI
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[config.CLAUDE_CLI_PATH, "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
health_status["dependencies"]["claude_cli"] = {
|
||||
"available": result.returncode == 0,
|
||||
"version": result.stdout.strip() if result.returncode == 0 else None,
|
||||
}
|
||||
except Exception as e:
|
||||
health_status["dependencies"]["claude_cli"] = {
|
||||
"available": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
# Check git
|
||||
health_status["dependencies"]["git"] = {
|
||||
"available": shutil.which("git") is not None,
|
||||
}
|
||||
|
||||
# Check Archon server connectivity (if configured)
|
||||
archon_server_url = os.getenv("ARCHON_SERVER_URL")
|
||||
if archon_server_url:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"{archon_server_url}/health")
|
||||
health_status["dependencies"]["archon_server"] = {
|
||||
"available": response.status_code == 200,
|
||||
"url": archon_server_url,
|
||||
}
|
||||
except Exception as e:
|
||||
health_status["dependencies"]["archon_server"] = {
|
||||
"available": False,
|
||||
"url": archon_server_url,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
# Check MCP server connectivity (if configured)
|
||||
archon_mcp_url = os.getenv("ARCHON_MCP_URL")
|
||||
if archon_mcp_url:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"{archon_mcp_url}/health")
|
||||
health_status["dependencies"]["archon_mcp"] = {
|
||||
"available": response.status_code == 200,
|
||||
"url": archon_mcp_url,
|
||||
}
|
||||
except Exception as e:
|
||||
health_status["dependencies"]["archon_mcp"] = {
|
||||
"available": False,
|
||||
"url": archon_mcp_url,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
# Determine overall status
|
||||
critical_deps_ok = (
|
||||
health_status["dependencies"].get("claude_cli", {}).get("available", False)
|
||||
and health_status["dependencies"].get("git", {}).get("available", False)
|
||||
)
|
||||
|
||||
if not critical_deps_ok:
|
||||
health_status["status"] = "degraded"
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> dict:
|
||||
"""Root endpoint with service information"""
|
||||
return {
|
||||
"service": "agent-work-orders",
|
||||
"version": "0.1.0",
|
||||
"description": "Independent agent work order service",
|
||||
"docs": "/docs",
|
||||
"health": "/health",
|
||||
"api": "/api/agent-work-orders",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
port = int(os.getenv("AGENT_WORK_ORDERS_PORT", "8053"))
|
||||
uvicorn.run(
|
||||
"src.agent_work_orders.server:app",
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
reload=True,
|
||||
)
|
||||
@@ -15,6 +15,7 @@ from ..models import (
|
||||
from ..sandbox_manager.sandbox_factory import SandboxFactory
|
||||
from ..state_manager.file_state_repository import FileStateRepository
|
||||
from ..state_manager.work_order_repository import WorkOrderRepository
|
||||
from ..utils.git_operations import get_commit_count, get_files_changed
|
||||
from ..utils.id_generator import generate_sandbox_identifier
|
||||
from ..utils.structured_logger import get_logger
|
||||
from . import workflow_operations
|
||||
@@ -158,16 +159,44 @@ class WorkflowOrchestrator:
|
||||
agent_work_order_id, result.output or ""
|
||||
)
|
||||
elif command_name == "create-pr":
|
||||
# Calculate git stats before marking as completed
|
||||
# Branch name is stored in context from create-branch step
|
||||
branch_name = context.get("create-branch")
|
||||
git_stats = await self._calculate_git_stats(
|
||||
branch_name,
|
||||
sandbox.get_working_directory()
|
||||
)
|
||||
|
||||
await self.state_repository.update_status(
|
||||
agent_work_order_id,
|
||||
AgentWorkOrderStatus.COMPLETED,
|
||||
github_pull_request_url=result.output,
|
||||
git_commit_count=git_stats["commit_count"],
|
||||
git_files_changed=git_stats["files_changed"],
|
||||
)
|
||||
# Save final step history
|
||||
await self.state_repository.save_step_history(agent_work_order_id, step_history)
|
||||
bound_logger.info("agent_work_order_completed", total_steps=len(step_history.steps))
|
||||
bound_logger.info(
|
||||
"agent_work_order_completed",
|
||||
total_steps=len(step_history.steps),
|
||||
git_commit_count=git_stats["commit_count"],
|
||||
git_files_changed=git_stats["files_changed"],
|
||||
)
|
||||
return # Exit early if PR created
|
||||
|
||||
# Calculate git stats for workflows that complete without PR
|
||||
branch_name = context.get("create-branch")
|
||||
if branch_name:
|
||||
git_stats = await self._calculate_git_stats(
|
||||
branch_name, sandbox.get_working_directory()
|
||||
)
|
||||
await self.state_repository.update_status(
|
||||
agent_work_order_id,
|
||||
AgentWorkOrderStatus.COMPLETED,
|
||||
git_commit_count=git_stats["commit_count"],
|
||||
git_files_changed=git_stats["files_changed"],
|
||||
)
|
||||
|
||||
# Save final step history
|
||||
await self.state_repository.save_step_history(agent_work_order_id, step_history)
|
||||
bound_logger.info("agent_work_order_completed", total_steps=len(step_history.steps))
|
||||
@@ -197,3 +226,35 @@ class WorkflowOrchestrator:
|
||||
error=str(cleanup_error),
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
async def _calculate_git_stats(
|
||||
self, branch_name: str | None, repo_path: str
|
||||
) -> dict[str, int]:
|
||||
"""Calculate git statistics for a branch
|
||||
|
||||
Args:
|
||||
branch_name: Name of the git branch
|
||||
repo_path: Path to the repository
|
||||
|
||||
Returns:
|
||||
Dictionary with commit_count and files_changed
|
||||
"""
|
||||
if not branch_name:
|
||||
return {"commit_count": 0, "files_changed": 0}
|
||||
|
||||
try:
|
||||
# Calculate stats compared to main branch
|
||||
commit_count = await get_commit_count(branch_name, repo_path)
|
||||
files_changed = await get_files_changed(branch_name, repo_path, base_branch="main")
|
||||
|
||||
return {
|
||||
"commit_count": commit_count,
|
||||
"files_changed": files_changed,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"git_stats_calculation_failed",
|
||||
branch_name=branch_name,
|
||||
error=str(e),
|
||||
)
|
||||
return {"commit_count": 0, "files_changed": 0}
|
||||
|
||||
141
python/src/server/api_routes/agent_work_orders_proxy.py
Normal file
141
python/src/server/api_routes/agent_work_orders_proxy.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Agent Work Orders API Gateway Proxy
|
||||
|
||||
Proxies requests from the main API to the independent agent work orders service.
|
||||
This provides a single API entry point for the frontend while maintaining service independence.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
|
||||
from ..config.service_discovery import get_agent_work_orders_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/agent-work-orders", tags=["agent-work-orders"])
|
||||
|
||||
|
||||
@router.api_route(
|
||||
"/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
response_class=Response,
|
||||
)
|
||||
async def proxy_to_agent_work_orders(request: Request, path: str = "") -> Response:
|
||||
"""Proxy all requests to the agent work orders microservice.
|
||||
|
||||
This acts as an API gateway, forwarding requests to the independent
|
||||
agent work orders service while maintaining a single API entry point.
|
||||
|
||||
Args:
|
||||
request: The incoming HTTP request
|
||||
path: The path segment to proxy (captured from URL)
|
||||
|
||||
Returns:
|
||||
Response from the agent work orders service with preserved headers and status
|
||||
|
||||
Raises:
|
||||
HTTPException: 503 if service unavailable, 504 if timeout, 500 for other errors
|
||||
"""
|
||||
# Get service URL from service discovery (outside try block for error handlers)
|
||||
service_url = get_agent_work_orders_url()
|
||||
|
||||
try:
|
||||
|
||||
# Build target URL
|
||||
target_path = f"/api/agent-work-orders/{path}" if path else "/api/agent-work-orders/"
|
||||
target_url = f"{service_url}{target_path}"
|
||||
|
||||
# Preserve query parameters
|
||||
query_string = str(request.url.query) if request.url.query else ""
|
||||
if query_string:
|
||||
target_url = f"{target_url}?{query_string}"
|
||||
|
||||
# Read request body
|
||||
body = await request.body()
|
||||
|
||||
# Prepare headers (exclude host and connection headers)
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in request.headers.items()
|
||||
if key.lower() not in ["host", "connection"]
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
f"Proxying {request.method} {request.url.path} to {target_url}",
|
||||
extra={
|
||||
"method": request.method,
|
||||
"source_path": request.url.path,
|
||||
"target_url": target_url,
|
||||
"query_params": query_string,
|
||||
},
|
||||
)
|
||||
|
||||
# Forward request to agent work orders service
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
content=body if body else None,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Proxy response: {response.status_code}",
|
||||
extra={
|
||||
"status_code": response.status_code,
|
||||
"target_url": target_url,
|
||||
},
|
||||
)
|
||||
|
||||
# Return response with preserved headers and status
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
media_type=response.headers.get("content-type"),
|
||||
)
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
logger.error(
|
||||
f"Agent work orders service unavailable at {service_url}",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"service_url": service_url,
|
||||
},
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Agent work orders service is currently unavailable",
|
||||
) from e
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
logger.error(
|
||||
f"Agent work orders service timeout",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"service_url": service_url,
|
||||
"target_url": target_url,
|
||||
},
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail="Agent work orders service request timed out",
|
||||
) from e
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error proxying to agent work orders service",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"service_url": service_url,
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
},
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Internal server error while contacting agent work orders service",
|
||||
) from e
|
||||
@@ -32,6 +32,7 @@ class ServiceDiscovery:
|
||||
server_port = os.getenv("ARCHON_SERVER_PORT")
|
||||
mcp_port = os.getenv("ARCHON_MCP_PORT")
|
||||
agents_port = os.getenv("ARCHON_AGENTS_PORT")
|
||||
agent_work_orders_port = os.getenv("AGENT_WORK_ORDERS_PORT")
|
||||
|
||||
if not server_port:
|
||||
raise ValueError(
|
||||
@@ -51,11 +52,18 @@ class ServiceDiscovery:
|
||||
"Please set it in your .env file or environment. "
|
||||
"Default value: 8052"
|
||||
)
|
||||
if not agent_work_orders_port:
|
||||
raise ValueError(
|
||||
"AGENT_WORK_ORDERS_PORT environment variable is required. "
|
||||
"Please set it in your .env file or environment. "
|
||||
"Default value: 8053"
|
||||
)
|
||||
|
||||
self.DEFAULT_PORTS = {
|
||||
"api": int(server_port),
|
||||
"mcp": int(mcp_port),
|
||||
"agents": int(agents_port),
|
||||
"agent_work_orders": int(agent_work_orders_port),
|
||||
}
|
||||
|
||||
self.environment = self._detect_environment()
|
||||
@@ -66,9 +74,11 @@ class ServiceDiscovery:
|
||||
"api": "archon-server",
|
||||
"mcp": "archon-mcp",
|
||||
"agents": "archon-agents",
|
||||
"agent_work_orders": "archon-agent-work-orders",
|
||||
"archon-server": "archon-server",
|
||||
"archon-mcp": "archon-mcp",
|
||||
"archon-agents": "archon-agents",
|
||||
"archon-agent-work-orders": "archon-agent-work-orders",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -225,6 +235,11 @@ def get_agents_url() -> str:
|
||||
return get_discovery().get_service_url("agents")
|
||||
|
||||
|
||||
def get_agent_work_orders_url() -> str:
|
||||
"""Get the Agent Work Orders service URL"""
|
||||
return get_discovery().get_service_url("agent_work_orders")
|
||||
|
||||
|
||||
async def is_service_healthy(service: str) -> bool:
|
||||
"""Check if a service is healthy"""
|
||||
return await get_discovery().health_check(service)
|
||||
@@ -238,5 +253,6 @@ __all__ = [
|
||||
"get_api_url",
|
||||
"get_mcp_url",
|
||||
"get_agents_url",
|
||||
"get_agent_work_orders_url",
|
||||
"is_service_healthy",
|
||||
]
|
||||
|
||||
@@ -19,6 +19,7 @@ from fastapi import FastAPI, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .api_routes.agent_chat_api import router as agent_chat_router
|
||||
from .api_routes.agent_work_orders_proxy import router as agent_work_orders_router
|
||||
from .api_routes.bug_report_api import router as bug_report_router
|
||||
from .api_routes.internal_api import router as internal_router
|
||||
from .api_routes.knowledge_api import router as knowledge_router
|
||||
@@ -189,17 +190,13 @@ app.include_router(ollama_router)
|
||||
app.include_router(projects_router)
|
||||
app.include_router(progress_router)
|
||||
app.include_router(agent_chat_router)
|
||||
app.include_router(agent_work_orders_router) # Proxy to independent agent work orders service
|
||||
app.include_router(internal_router)
|
||||
app.include_router(bug_report_router)
|
||||
app.include_router(providers_router)
|
||||
app.include_router(version_router)
|
||||
app.include_router(migration_router)
|
||||
|
||||
# Mount Agent Work Orders sub-application
|
||||
from src.agent_work_orders.main import app as agent_work_orders_app
|
||||
|
||||
app.mount("/api/agent-work-orders", agent_work_orders_app)
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
|
||||
Reference in New Issue
Block a user