Files
hive/backend/app/api/cli_agents.py
anthonyrawlins 4de45bf450 Merge redundant coordinators into unified coordinator architecture
Major refactoring:
- Created UnifiedCoordinator that combines HiveCoordinator and DistributedCoordinator
- Eliminated code duplication and architectural redundancy
- Unified agent management, task orchestration, and workflow execution
- Single coordinator instance replaces two global coordinators
- Backward compatibility maintained through state aliases

Key features of UnifiedCoordinator:
 Combined agent types: Ollama + CLI agents with unified management
 Dual task modes: Simple tasks + complex distributed workflows
 Performance monitoring: Prometheus metrics + adaptive load balancing
 Background processes: Health monitoring + performance optimization
 Redis integration: Distributed caching and coordination (optional)
 Database integration: Agent loading + task persistence preparation

API updates:
- Updated all API endpoints to use unified coordinator
- Maintained interface compatibility for existing endpoints
- Fixed attribute references for unified agent model
- Simplified dependency injection pattern

Architecture benefits:
- Single point of coordination eliminates race conditions
- Reduced memory footprint (one coordinator vs two)
- Simplified initialization and lifecycle management
- Consistent feature set across all orchestration modes
- Better separation of concerns within single coordinator class

This resolves the critical architectural issue of redundant coordinators
while maintaining full backward compatibility and adding enhanced features.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 08:44:21 +10:00

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.unified_coordinator import UnifiedCoordinator, 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
}