- Parameterize CORS_ORIGINS in docker-compose.swarm.yml - Add .env.example with configuration options - Create comprehensive LOCAL_DEVELOPMENT.md guide - Update README.md with environment variable documentation - Provide alternatives for local development without production domain 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""
|
|
CLI Agents API endpoints
|
|
Provides REST API for managing CLI-based agents in the Hive system.
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
from sqlalchemy.orm import Session
|
|
from typing import Dict, Any, List
|
|
from pydantic import BaseModel
|
|
|
|
from ..core.database import get_db
|
|
from ..models.agent import Agent as ORMAgent
|
|
from ..core.hive_coordinator import HiveCoordinator, Agent, AgentType
|
|
from ..cli_agents.cli_agent_manager import get_cli_agent_manager
|
|
|
|
router = APIRouter(prefix="/api/cli-agents", tags=["cli-agents"])
|
|
|
|
|
|
class CliAgentRegistration(BaseModel):
|
|
"""Request model for CLI agent registration"""
|
|
id: str
|
|
host: str
|
|
node_version: str
|
|
model: str = "gemini-2.5-pro"
|
|
specialization: str = "general_ai"
|
|
max_concurrent: int = 2
|
|
agent_type: str = "gemini" # CLI agent type (gemini, etc.)
|
|
command_timeout: int = 60
|
|
ssh_timeout: int = 5
|
|
|
|
|
|
class CliAgentResponse(BaseModel):
|
|
"""Response model for CLI agent operations"""
|
|
id: str
|
|
endpoint: str
|
|
model: str
|
|
specialization: str
|
|
agent_type: str
|
|
cli_config: Dict[str, Any]
|
|
status: str
|
|
max_concurrent: int
|
|
current_tasks: int
|
|
|
|
|
|
@router.post("/register", response_model=Dict[str, Any])
|
|
async def register_cli_agent(
|
|
agent_data: CliAgentRegistration,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Register a new CLI agent"""
|
|
|
|
# Check if agent already exists
|
|
existing_agent = db.query(ORMAgent).filter(ORMAgent.id == agent_data.id).first()
|
|
if existing_agent:
|
|
raise HTTPException(status_code=400, detail=f"Agent {agent_data.id} already exists")
|
|
|
|
try:
|
|
# Get CLI agent manager
|
|
cli_manager = get_cli_agent_manager()
|
|
|
|
# Create CLI configuration
|
|
cli_config = {
|
|
"host": agent_data.host,
|
|
"node_version": agent_data.node_version,
|
|
"model": agent_data.model,
|
|
"specialization": agent_data.specialization,
|
|
"max_concurrent": agent_data.max_concurrent,
|
|
"command_timeout": agent_data.command_timeout,
|
|
"ssh_timeout": agent_data.ssh_timeout,
|
|
"agent_type": agent_data.agent_type
|
|
}
|
|
|
|
# Test CLI agent connectivity before registration (optional for development)
|
|
health = {"cli_healthy": True, "test_skipped": True}
|
|
try:
|
|
test_agent = cli_manager.cli_factory.create_agent(f"test-{agent_data.id}", cli_config)
|
|
health = await test_agent.health_check()
|
|
await test_agent.cleanup() # Clean up test agent
|
|
|
|
if not health.get("cli_healthy", False):
|
|
print(f"⚠️ CLI agent connectivity test failed for {agent_data.host}, but proceeding with registration")
|
|
health["cli_healthy"] = False
|
|
health["warning"] = f"Connectivity test failed for {agent_data.host}"
|
|
except Exception as e:
|
|
print(f"⚠️ CLI agent connectivity test error for {agent_data.host}: {e}, proceeding anyway")
|
|
health = {"cli_healthy": False, "error": str(e), "test_skipped": True}
|
|
|
|
# Map specialization to Hive AgentType
|
|
specialization_mapping = {
|
|
"general_ai": AgentType.GENERAL_AI,
|
|
"reasoning": AgentType.REASONING,
|
|
"code_analysis": AgentType.PROFILER,
|
|
"documentation": AgentType.DOCS_WRITER,
|
|
"testing": AgentType.TESTER,
|
|
"cli_gemini": AgentType.CLI_GEMINI
|
|
}
|
|
|
|
hive_specialty = specialization_mapping.get(agent_data.specialization, AgentType.GENERAL_AI)
|
|
|
|
# Create Hive Agent object
|
|
hive_agent = Agent(
|
|
id=agent_data.id,
|
|
endpoint=f"cli://{agent_data.host}",
|
|
model=agent_data.model,
|
|
specialty=hive_specialty,
|
|
max_concurrent=agent_data.max_concurrent,
|
|
current_tasks=0,
|
|
agent_type="cli",
|
|
cli_config=cli_config
|
|
)
|
|
|
|
# Register with Hive coordinator (this will also register with CLI manager)
|
|
# For now, we'll register directly in the database
|
|
db_agent = ORMAgent(
|
|
id=hive_agent.id,
|
|
name=f"{agent_data.host}-{agent_data.agent_type}",
|
|
endpoint=hive_agent.endpoint,
|
|
model=hive_agent.model,
|
|
specialty=hive_agent.specialty.value,
|
|
specialization=hive_agent.specialty.value, # For compatibility
|
|
max_concurrent=hive_agent.max_concurrent,
|
|
current_tasks=hive_agent.current_tasks,
|
|
agent_type=hive_agent.agent_type,
|
|
cli_config=hive_agent.cli_config
|
|
)
|
|
|
|
db.add(db_agent)
|
|
db.commit()
|
|
db.refresh(db_agent)
|
|
|
|
# Register with CLI manager
|
|
cli_manager.create_cli_agent(agent_data.id, cli_config)
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": f"CLI agent {agent_data.id} registered successfully",
|
|
"agent_id": agent_data.id,
|
|
"endpoint": hive_agent.endpoint,
|
|
"health_check": health
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to register CLI agent: {str(e)}")
|
|
|
|
|
|
@router.get("/", response_model=List[CliAgentResponse])
|
|
async def list_cli_agents(db: Session = Depends(get_db)):
|
|
"""List all CLI agents"""
|
|
|
|
cli_agents = db.query(ORMAgent).filter(ORMAgent.agent_type == "cli").all()
|
|
|
|
return [
|
|
CliAgentResponse(
|
|
id=agent.id,
|
|
endpoint=agent.endpoint,
|
|
model=agent.model,
|
|
specialization=agent.specialty,
|
|
agent_type=agent.agent_type,
|
|
cli_config=agent.cli_config or {},
|
|
status="active", # TODO: Get actual status from CLI manager
|
|
max_concurrent=agent.max_concurrent,
|
|
current_tasks=agent.current_tasks
|
|
)
|
|
for agent in cli_agents
|
|
]
|
|
|
|
|
|
@router.get("/{agent_id}", response_model=CliAgentResponse)
|
|
async def get_cli_agent(agent_id: str, db: Session = Depends(get_db)):
|
|
"""Get details of a specific CLI agent"""
|
|
|
|
agent = db.query(ORMAgent).filter(
|
|
ORMAgent.id == agent_id,
|
|
ORMAgent.agent_type == "cli"
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"CLI agent {agent_id} not found")
|
|
|
|
return CliAgentResponse(
|
|
id=agent.id,
|
|
endpoint=agent.endpoint,
|
|
model=agent.model,
|
|
specialization=agent.specialty,
|
|
agent_type=agent.agent_type,
|
|
cli_config=agent.cli_config or {},
|
|
status="active", # TODO: Get actual status from CLI manager
|
|
max_concurrent=agent.max_concurrent,
|
|
current_tasks=agent.current_tasks
|
|
)
|
|
|
|
|
|
@router.post("/{agent_id}/health-check")
|
|
async def health_check_cli_agent(agent_id: str, db: Session = Depends(get_db)):
|
|
"""Perform health check on a CLI agent"""
|
|
|
|
agent = db.query(ORMAgent).filter(
|
|
ORMAgent.id == agent_id,
|
|
ORMAgent.agent_type == "cli"
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"CLI agent {agent_id} not found")
|
|
|
|
try:
|
|
cli_manager = get_cli_agent_manager()
|
|
cli_agent = cli_manager.get_cli_agent(agent_id)
|
|
|
|
if not cli_agent:
|
|
raise HTTPException(status_code=404, detail=f"CLI agent {agent_id} not active in manager")
|
|
|
|
health = await cli_agent.health_check()
|
|
return health
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Health check failed: {str(e)}")
|
|
|
|
|
|
@router.get("/statistics/all")
|
|
async def get_all_cli_agent_statistics():
|
|
"""Get statistics for all CLI agents"""
|
|
|
|
try:
|
|
cli_manager = get_cli_agent_manager()
|
|
stats = cli_manager.get_agent_statistics()
|
|
return stats
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get statistics: {str(e)}")
|
|
|
|
|
|
@router.delete("/{agent_id}")
|
|
async def unregister_cli_agent(agent_id: str, db: Session = Depends(get_db)):
|
|
"""Unregister a CLI agent"""
|
|
|
|
agent = db.query(ORMAgent).filter(
|
|
ORMAgent.id == agent_id,
|
|
ORMAgent.agent_type == "cli"
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"CLI agent {agent_id} not found")
|
|
|
|
try:
|
|
# Remove from CLI manager if it exists
|
|
cli_manager = get_cli_agent_manager()
|
|
cli_agent = cli_manager.get_cli_agent(agent_id)
|
|
if cli_agent:
|
|
await cli_agent.cleanup()
|
|
cli_manager.active_agents.pop(agent_id, None)
|
|
|
|
# Remove from database
|
|
db.delete(agent)
|
|
db.commit()
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": f"CLI agent {agent_id} unregistered successfully"
|
|
}
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to unregister CLI agent: {str(e)}")
|
|
|
|
|
|
@router.post("/register-predefined")
|
|
async def register_predefined_cli_agents(db: Session = Depends(get_db)):
|
|
"""Register predefined CLI agents (walnut-gemini, ironwood-gemini)"""
|
|
|
|
predefined_configs = [
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440001", # walnut-gemini UUID
|
|
"host": "walnut",
|
|
"node_version": "v22.14.0",
|
|
"model": "gemini-2.5-pro",
|
|
"specialization": "general_ai",
|
|
"max_concurrent": 2,
|
|
"agent_type": "gemini"
|
|
},
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440002", # ironwood-gemini UUID
|
|
"host": "ironwood",
|
|
"node_version": "v22.17.0",
|
|
"model": "gemini-2.5-pro",
|
|
"specialization": "reasoning",
|
|
"max_concurrent": 2,
|
|
"agent_type": "gemini"
|
|
},
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440003", # rosewood-gemini UUID
|
|
"host": "rosewood",
|
|
"node_version": "v22.17.0",
|
|
"model": "gemini-2.5-pro",
|
|
"specialization": "cli_gemini",
|
|
"max_concurrent": 2,
|
|
"agent_type": "gemini"
|
|
}
|
|
]
|
|
|
|
results = []
|
|
|
|
for config in predefined_configs:
|
|
try:
|
|
# Check if already exists
|
|
existing = db.query(ORMAgent).filter(ORMAgent.id == config["id"]).first()
|
|
if existing:
|
|
results.append({
|
|
"agent_id": config["id"],
|
|
"status": "already_exists",
|
|
"message": f"Agent {config['id']} already registered"
|
|
})
|
|
continue
|
|
|
|
# Register agent
|
|
agent_data = CliAgentRegistration(**config)
|
|
result = await register_cli_agent(agent_data, db)
|
|
results.append(result)
|
|
|
|
except Exception as e:
|
|
results.append({
|
|
"agent_id": config["id"],
|
|
"status": "failed",
|
|
"error": str(e)
|
|
})
|
|
|
|
return {
|
|
"status": "completed",
|
|
"results": results
|
|
} |