feat: add agent work orders microservice with hybrid deployment

This commit is contained in:
Rasmus Widing
2025-10-23 12:46:57 +03:00
parent 8f3e8bc220
commit f07cefd1a1
23 changed files with 1741 additions and 93 deletions

View 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

View 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

View File

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

View File

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

View 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,
)

View File

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

View 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

View File

@@ -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",
]

View File

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