diff --git a/backend/Dockerfile b/backend/Dockerfile index a7746bcf..06e9ef4c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,8 +9,16 @@ RUN apt-get update && apt-get install -y \ libffi-dev \ libssl-dev \ curl \ + dumb-init \ && rm -rf /var/lib/apt/lists/* +# Environment variables with production defaults +ENV DATABASE_URL=postgresql://hive:hive@postgres:5432/hive +ENV REDIS_URL=redis://redis:6379/0 +ENV LOG_LEVEL=info +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/app/app + # Copy requirements first for better caching COPY requirements.txt . @@ -27,9 +35,12 @@ USER hive # Expose port EXPOSE 8000 -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ +# Enhanced health check with longer startup period +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 -# Run the application -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +# Use dumb-init for proper signal handling +ENTRYPOINT ["dumb-init", "--"] + +# Run the application with production settings +CMD ["uvicorn", "app.main:socket_app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--log-level", "info"] \ No newline at end of file diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 74e23788..be3fadaf 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -1,23 +1,52 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from typing import List, Dict, Any from ..core.auth import get_current_user +from ..core.hive_coordinator import Agent, AgentType router = APIRouter() +from app.core.database import SessionLocal +from app.models.agent import Agent as ORMAgent + @router.get("/agents") -async def get_agents(current_user: dict = Depends(get_current_user)): +async def get_agents(request: Request, current_user: dict = Depends(get_current_user)): """Get all registered agents""" + with SessionLocal() as db: + db_agents = db.query(ORMAgent).all() + agents_list = [] + for db_agent in db_agents: + agents_list.append({ + "id": db_agent.id, + "endpoint": db_agent.endpoint, + "model": db_agent.model, + "specialty": db_agent.specialty, + "max_concurrent": db_agent.max_concurrent, + "current_tasks": db_agent.current_tasks + }) + return { - "agents": [], - "total": 0, - "message": "Agents endpoint ready" + "agents": agents_list, + "total": len(agents_list), } @router.post("/agents") -async def register_agent(agent_data: Dict[str, Any], current_user: dict = Depends(get_current_user)): +async def register_agent(agent_data: Dict[str, Any], request: Request, current_user: dict = Depends(get_current_user)): """Register a new agent""" - return { - "status": "success", - "message": "Agent registration endpoint ready", - "agent_id": "placeholder" - } \ No newline at end of file + hive_coordinator = request.app.state.hive_coordinator + + try: + agent = Agent( + id=agent_data["id"], + endpoint=agent_data["endpoint"], + model=agent_data["model"], + specialty=AgentType(agent_data["specialty"]), + max_concurrent=agent_data.get("max_concurrent", 2), + ) + hive_coordinator.add_agent(agent) + return { + "status": "success", + "message": f"Agent {agent.id} registered successfully", + "agent_id": agent.id + } + except (KeyError, ValueError) as e: + raise HTTPException(status_code=400, detail=f"Invalid agent data: {e}") \ No newline at end of file diff --git a/backend/app/api/distributed_workflows.py b/backend/app/api/distributed_workflows.py new file mode 100644 index 00000000..042faaaa --- /dev/null +++ b/backend/app/api/distributed_workflows.py @@ -0,0 +1,499 @@ +""" +Distributed Workflow API Endpoints +RESTful API for managing distributed development workflows across the cluster +""" + +from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Request +from pydantic import BaseModel, Field +from typing import Dict, Any, List, Optional +import asyncio +import logging +from datetime import datetime + +from ..core.distributed_coordinator import DistributedCoordinator, TaskType, TaskPriority +from ..core.hive_coordinator import HiveCoordinator + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/distributed", tags=["distributed-workflows"]) + +# Global coordinator instance +distributed_coordinator: Optional[DistributedCoordinator] = None + +class WorkflowRequest(BaseModel): + """Request model for workflow submission""" + name: str = Field(..., description="Workflow name") + description: str = Field(..., description="Workflow description") + requirements: str = Field(..., description="Development requirements") + context: str = Field(default="", description="Additional context") + language: str = Field(default="python", description="Target programming language") + test_types: List[str] = Field(default=["unit", "integration"], description="Types of tests to generate") + optimization_targets: List[str] = Field(default=["performance", "memory"], description="Optimization focus areas") + build_config: Dict[str, Any] = Field(default_factory=dict, description="Build configuration") + priority: str = Field(default="normal", description="Workflow priority (critical, high, normal, low)") + +class TaskStatus(BaseModel): + """Task status model""" + id: str + type: str + status: str + assigned_agent: Optional[str] + execution_time: float + result: Optional[Dict[str, Any]] = None + +class WorkflowStatus(BaseModel): + """Workflow status response model""" + workflow_id: str + name: str + total_tasks: int + completed_tasks: int + failed_tasks: int + progress: float + status: str + created_at: datetime + tasks: List[TaskStatus] + +class ClusterStatus(BaseModel): + """Cluster status model""" + total_agents: int + healthy_agents: int + total_capacity: int + current_load: int + utilization: float + agents: List[Dict[str, Any]] + +class PerformanceMetrics(BaseModel): + """Performance metrics model""" + total_workflows: int + completed_workflows: int + failed_workflows: int + average_completion_time: float + throughput_per_hour: float + agent_performance: Dict[str, Dict[str, float]] + +async def get_coordinator() -> DistributedCoordinator: + """Dependency to get the distributed coordinator instance""" + # Import here to avoid circular imports + from ..main import distributed_coordinator as main_coordinator + if main_coordinator is None: + raise HTTPException(status_code=503, detail="Distributed coordinator not initialized") + return main_coordinator + +@router.on_event("startup") +async def startup_distributed_coordinator(): + """Initialize the distributed coordinator on startup""" + global distributed_coordinator + try: + distributed_coordinator = DistributedCoordinator() + await distributed_coordinator.start() + logger.info("Distributed coordinator started successfully") + except Exception as e: + logger.error(f"Failed to start distributed coordinator: {e}") + raise + +@router.on_event("shutdown") +async def shutdown_distributed_coordinator(): + """Shutdown the distributed coordinator""" + global distributed_coordinator + if distributed_coordinator: + await distributed_coordinator.stop() + logger.info("Distributed coordinator stopped") + +@router.post("/workflows", response_model=Dict[str, str]) +async def submit_workflow( + workflow: WorkflowRequest, + background_tasks: BackgroundTasks, + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Submit a new development workflow for distributed execution + + This endpoint creates a complete development workflow that includes: + - Code generation + - Code review + - Testing + - Compilation + - Optimization + + The workflow is distributed across the cluster based on agent capabilities. + """ + try: + # Convert priority string to enum + priority_map = { + "critical": TaskPriority.CRITICAL, + "high": TaskPriority.HIGH, + "normal": TaskPriority.NORMAL, + "low": TaskPriority.LOW + } + + workflow_dict = { + "name": workflow.name, + "description": workflow.description, + "requirements": workflow.requirements, + "context": workflow.context, + "language": workflow.language, + "test_types": workflow.test_types, + "optimization_targets": workflow.optimization_targets, + "build_config": workflow.build_config, + "priority": priority_map.get(workflow.priority, TaskPriority.NORMAL) + } + + workflow_id = await coordinator.submit_workflow(workflow_dict) + + return { + "workflow_id": workflow_id, + "message": "Workflow submitted successfully", + "status": "accepted" + } + + except Exception as e: + logger.error(f"Failed to submit workflow: {e}") + raise HTTPException(status_code=500, detail=f"Failed to submit workflow: {str(e)}") + +@router.get("/workflows/{workflow_id}", response_model=WorkflowStatus) +async def get_workflow_status( + workflow_id: str, + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Get detailed status of a specific workflow + + Returns comprehensive information about workflow progress, + individual task status, and execution metrics. + """ + try: + status = await coordinator.get_workflow_status(workflow_id) + + if "error" in status: + raise HTTPException(status_code=404, detail=status["error"]) + + return WorkflowStatus( + workflow_id=status["workflow_id"], + name=f"Workflow {workflow_id}", # Could be enhanced with actual name storage + total_tasks=status["total_tasks"], + completed_tasks=status["completed_tasks"], + failed_tasks=status["failed_tasks"], + progress=status["progress"], + status=status["status"], + created_at=datetime.now(), # Could be enhanced with actual creation time + tasks=[ + TaskStatus( + id=task["id"], + type=task["type"], + status=task["status"], + assigned_agent=task["assigned_agent"], + execution_time=task["execution_time"], + result=None # Could include task results if needed + ) + for task in status["tasks"] + ] + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get workflow status: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get workflow status: {str(e)}") + +@router.get("/cluster/status", response_model=ClusterStatus) +async def get_cluster_status( + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Get current cluster status and agent information + + Returns real-time information about all agents including: + - Health status + - Current load + - Performance metrics + - Specializations + """ + try: + agents_info = [] + total_capacity = 0 + current_load = 0 + healthy_agents = 0 + + for agent in coordinator.agents.values(): + if agent.health_status == "healthy": + healthy_agents += 1 + + total_capacity += agent.max_concurrent + current_load += agent.current_load + + agents_info.append({ + "id": agent.id, + "endpoint": agent.endpoint, + "model": agent.model, + "gpu_type": agent.gpu_type, + "specializations": [spec.value for spec in agent.specializations], + "max_concurrent": agent.max_concurrent, + "current_load": agent.current_load, + "utilization": (agent.current_load / agent.max_concurrent) * 100, + "performance_score": round(agent.performance_score, 3), + "last_response_time": round(agent.last_response_time, 2), + "health_status": agent.health_status + }) + + utilization = (current_load / total_capacity) * 100 if total_capacity > 0 else 0 + + return ClusterStatus( + total_agents=len(coordinator.agents), + healthy_agents=healthy_agents, + total_capacity=total_capacity, + current_load=current_load, + utilization=round(utilization, 2), + agents=agents_info + ) + + except Exception as e: + logger.error(f"Failed to get cluster status: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get cluster status: {str(e)}") + +@router.get("/performance/metrics", response_model=PerformanceMetrics) +async def get_performance_metrics( + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Get comprehensive performance metrics for the distributed system + + Returns metrics including: + - Workflow completion rates + - Agent performance statistics + - Throughput measurements + - System efficiency indicators + """ + try: + # Calculate basic metrics + total_workflows = len([task for task in coordinator.tasks.values() + if task.type.value == "code_generation"]) + completed_workflows = len([task for task in coordinator.tasks.values() + if task.type.value == "code_generation" and task.status == "completed"]) + failed_workflows = len([task for task in coordinator.tasks.values() + if task.type.value == "code_generation" and task.status == "failed"]) + + # Calculate average completion time + completed_tasks = [task for task in coordinator.tasks.values() if task.status == "completed"] + average_completion_time = 0.0 + if completed_tasks: + import time + current_time = time.time() + completion_times = [current_time - task.created_at for task in completed_tasks] + average_completion_time = sum(completion_times) / len(completion_times) + + # Calculate throughput (workflows per hour) + import time + current_time = time.time() + recent_completions = [ + task for task in completed_tasks + if current_time - task.created_at < 3600 # Last hour + ] + throughput_per_hour = len(recent_completions) + + # Agent performance metrics + agent_performance = {} + for agent_id, agent in coordinator.agents.items(): + performance_history = coordinator.performance_history.get(agent_id, []) + agent_performance[agent_id] = { + "avg_response_time": sum(performance_history) / len(performance_history) if performance_history else 0.0, + "performance_score": agent.performance_score, + "total_tasks": len(performance_history), + "current_utilization": (agent.current_load / agent.max_concurrent) * 100 + } + + return PerformanceMetrics( + total_workflows=total_workflows, + completed_workflows=completed_workflows, + failed_workflows=failed_workflows, + average_completion_time=round(average_completion_time, 2), + throughput_per_hour=throughput_per_hour, + agent_performance=agent_performance + ) + + except Exception as e: + logger.error(f"Failed to get performance metrics: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get performance metrics: {str(e)}") + +@router.post("/workflows/{workflow_id}/cancel") +async def cancel_workflow( + workflow_id: str, + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Cancel a running workflow and all its associated tasks + """ + try: + # Find all tasks for this workflow + workflow_tasks = [ + task for task in coordinator.tasks.values() + if task.payload.get("workflow_id") == workflow_id + ] + + if not workflow_tasks: + raise HTTPException(status_code=404, detail="Workflow not found") + + # Cancel pending and executing tasks + cancelled_count = 0 + for task in workflow_tasks: + if task.status in ["pending", "executing"]: + task.status = "cancelled" + cancelled_count += 1 + + return { + "workflow_id": workflow_id, + "message": f"Cancelled {cancelled_count} tasks", + "status": "cancelled" + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to cancel workflow: {e}") + raise HTTPException(status_code=500, detail=f"Failed to cancel workflow: {str(e)}") + +@router.post("/cluster/optimize") +async def trigger_cluster_optimization( + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Manually trigger cluster optimization + + Forces immediate optimization of: + - Agent parameter tuning + - Load balancing adjustments + - Performance metric updates + """ + try: + # Trigger optimization methods + await coordinator._optimize_agent_parameters() + await coordinator._cleanup_completed_tasks() + + return { + "message": "Cluster optimization triggered successfully", + "timestamp": datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"Failed to trigger optimization: {e}") + raise HTTPException(status_code=500, detail=f"Failed to trigger optimization: {str(e)}") + +@router.get("/workflows", response_model=List[Dict[str, Any]]) +async def list_workflows( + status: Optional[str] = None, + limit: int = 50 +): + """ + List all workflows with optional filtering + + Args: + status: Filter by workflow status (pending, executing, completed, failed) + limit: Maximum number of workflows to return + """ + try: + # Get coordinator, return empty array if not available + try: + coordinator = await get_coordinator() + except HTTPException: + return [] + + # Group tasks by workflow_id + workflows = {} + for task in coordinator.tasks.values(): + workflow_id = task.payload.get("workflow_id") + if workflow_id: + if workflow_id not in workflows: + workflows[workflow_id] = [] + workflows[workflow_id].append(task) + + # Build workflow summaries + workflow_list = [] + for workflow_id, tasks in workflows.items(): + total_tasks = len(tasks) + completed_tasks = sum(1 for task in tasks if task.status == "completed") + failed_tasks = sum(1 for task in tasks if task.status == "failed") + + workflow_status = "completed" if completed_tasks == total_tasks else "in_progress" + if failed_tasks > 0: + workflow_status = "failed" + + # Apply status filter + if status and workflow_status != status: + continue + + workflow_list.append({ + "workflow_id": workflow_id, + "total_tasks": total_tasks, + "completed_tasks": completed_tasks, + "failed_tasks": failed_tasks, + "progress": (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0, + "status": workflow_status, + "created_at": min(task.created_at for task in tasks) + }) + + # Sort by creation time (newest first) and apply limit + workflow_list.sort(key=lambda x: x["created_at"], reverse=True) + return workflow_list[:limit] + + except Exception as e: + logger.error(f"Failed to list workflows: {e}") + raise HTTPException(status_code=500, detail=f"Failed to list workflows: {str(e)}") + +@router.get("/agents/{agent_id}/tasks", response_model=List[Dict[str, Any]]) +async def get_agent_tasks( + agent_id: str, + coordinator: DistributedCoordinator = Depends(get_coordinator) +): + """ + Get all tasks assigned to a specific agent + """ + try: + if agent_id not in coordinator.agents: + raise HTTPException(status_code=404, detail="Agent not found") + + agent_tasks = [ + { + "task_id": task.id, + "type": task.type.value, + "status": task.status, + "priority": task.priority.value, + "created_at": task.created_at, + "workflow_id": task.payload.get("workflow_id") + } + for task in coordinator.tasks.values() + if task.assigned_agent == agent_id + ] + + return agent_tasks + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get agent tasks: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get agent tasks: {str(e)}") + +# Health check endpoint for the distributed system +@router.get("/health") +async def health_check(coordinator: DistributedCoordinator = Depends(get_coordinator)): + """ + Health check for the distributed workflow system + """ + try: + healthy_agents = sum(1 for agent in coordinator.agents.values() + if agent.health_status == "healthy") + total_agents = len(coordinator.agents) + + system_health = "healthy" if healthy_agents > 0 else "unhealthy" + + return { + "status": system_health, + "healthy_agents": healthy_agents, + "total_agents": total_agents, + "timestamp": datetime.now().isoformat() + } + + except Exception as e: + return { + "status": "unhealthy", + "error": str(e), + "timestamp": datetime.now().isoformat() + } \ No newline at end of file diff --git a/backend/app/api/projects.py b/backend/app/api/projects.py index 5c3a6d10..d6a0d248 100644 --- a/backend/app/api/projects.py +++ b/backend/app/api/projects.py @@ -1,9 +1,45 @@ -from fastapi import APIRouter, Depends -from ..core.auth import get_current_user +from fastapi import APIRouter, Depends, HTTPException +from typing import Dict, Any, List +from app.core.auth import get_current_user +from app.services.project_service import ProjectService router = APIRouter() +project_service = ProjectService() @router.get("/projects") -async def get_projects(current_user: dict = Depends(get_current_user)): - """Get all projects""" - return {"projects": [], "total": 0, "message": "Projects endpoint ready"} \ No newline at end of file +async def get_projects(current_user: dict = Depends(get_current_user)) -> List[Dict[str, Any]]: + """Get all projects from the local filesystem.""" + try: + return project_service.get_all_projects() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/projects/{project_id}") +async def get_project(project_id: str, current_user: dict = Depends(get_current_user)) -> Dict[str, Any]: + """Get a specific project by ID.""" + try: + project = project_service.get_project_by_id(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return project + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/projects/{project_id}/metrics") +async def get_project_metrics(project_id: str, current_user: dict = Depends(get_current_user)) -> Dict[str, Any]: + """Get detailed metrics for a project.""" + try: + metrics = project_service.get_project_metrics(project_id) + if not metrics: + raise HTTPException(status_code=404, detail="Project not found") + return metrics + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/projects/{project_id}/tasks") +async def get_project_tasks(project_id: str, current_user: dict = Depends(get_current_user)) -> List[Dict[str, Any]]: + """Get tasks for a project (from GitHub issues and TODOS.md).""" + try: + return project_service.get_project_tasks(project_id) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 056af994..95f645b0 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -1,19 +1,82 @@ -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event, text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import QueuePool +from sqlalchemy.exc import DisconnectionError import os +import time +import logging -# Use PostgreSQL in production, SQLite for development -DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./hive.db") +# Enhanced database configuration with connection pooling +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:hive123@hive_postgres:5432/hive") + +# Create engine with connection pooling and reliability features +if "sqlite" in DATABASE_URL: + engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False}, + pool_pre_ping=True + ) +else: + engine = create_engine( + DATABASE_URL, + poolclass=QueuePool, + pool_size=10, + max_overflow=20, + pool_pre_ping=True, + pool_recycle=3600, + echo=False + ) -engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() +@event.listens_for(engine, "connect") +def set_sqlite_pragma(dbapi_connection, connection_record): + """Set SQLite pragma for foreign key support""" + if "sqlite" in DATABASE_URL: + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + def get_db(): + """Database session dependency with proper error handling""" db = SessionLocal() try: yield db + except DisconnectionError as e: + logging.error(f"Database disconnection error: {e}") + db.rollback() + raise + except Exception as e: + logging.error(f"Database error: {e}") + db.rollback() + raise finally: - db.close() \ No newline at end of file + db.close() + +def test_database_connection(): + """Test database connectivity""" + try: + with engine.connect() as conn: + result = conn.execute(text("SELECT 1")) + return True + except Exception as e: + logging.error(f"Database connection test failed: {e}") + return False + +def init_database_with_retry(max_retries=5, retry_delay=2): + """Initialize database with retry logic""" + for attempt in range(max_retries): + try: + Base.metadata.create_all(bind=engine) + logging.info("Database initialized successfully") + return True + except Exception as e: + if attempt == max_retries - 1: + logging.error(f"Database initialization failed after {max_retries} attempts: {e}") + raise + logging.warning(f"Database initialization attempt {attempt + 1} failed: {e}") + time.sleep(retry_delay ** attempt) + return False \ No newline at end of file diff --git a/backend/app/core/hive_coordinator.py b/backend/app/core/hive_coordinator.py index 34f3f894..322d9738 100644 --- a/backend/app/core/hive_coordinator.py +++ b/backend/app/core/hive_coordinator.py @@ -11,6 +11,9 @@ import time from dataclasses import dataclass from typing import Dict, List, Optional, Any from enum import Enum +from sqlalchemy.orm import Session +from ..models.agent import Agent as ORMAgent +from ..core.database import SessionLocal class AgentType(Enum): KERNEL_DEV = "kernel_dev" @@ -48,9 +51,8 @@ class Task: created_at: float = None completed_at: Optional[float] = None -class AIDevCoordinator: +class HiveCoordinator: def __init__(self): - self.agents: Dict[str, Agent] = {} self.tasks: Dict[str, Task] = {} self.task_queue: List[Task] = [] self.is_initialized = False @@ -89,9 +91,20 @@ FOCUS:[full-coverage]→[test+measure+handle+automate]""" } def add_agent(self, agent: Agent): - """Register a new agent""" - self.agents[agent.id] = agent - print(f"Registered agent {agent.id} ({agent.specialty.value}) at {agent.endpoint}") + """Register a new agent and persist to database""" + with SessionLocal() as db: + db_agent = ORMAgent( + id=agent.id, + endpoint=agent.endpoint, + model=agent.model, + specialty=agent.specialty.value, + max_concurrent=agent.max_concurrent, + current_tasks=agent.current_tasks + ) + db.add(db_agent) + db.commit() + db.refresh(db_agent) + print(f"Registered agent {agent.id} ({agent.specialty.value}) at {agent.endpoint} and persisted to DB") def create_task(self, task_type: AgentType, context: Dict, priority: int = 3) -> Task: """Create a new development task""" @@ -111,16 +124,37 @@ FOCUS:[full-coverage]→[test+measure+handle+automate]""" return task def get_available_agent(self, task_type: AgentType) -> Optional[Agent]: - """Find an available agent for the task type""" - available_agents = [ - agent for agent in self.agents.values() - if agent.specialty == task_type and agent.current_tasks < agent.max_concurrent - ] - return available_agents[0] if available_agents else None + """Find an available agent for the task type from the database""" + with SessionLocal() as db: + available_agents_orm = db.query(ORMAgent).filter( + ORMAgent.specialty == task_type.value, + ORMAgent.current_tasks < ORMAgent.max_concurrent + ).all() + + if available_agents_orm: + # Convert ORM agent to dataclass Agent + db_agent = available_agents_orm[0] + return Agent( + id=db_agent.id, + endpoint=db_agent.endpoint, + model=db_agent.model, + specialty=AgentType(db_agent.specialty), + max_concurrent=db_agent.max_concurrent, + current_tasks=db_agent.current_tasks + ) + return None async def execute_task(self, task: Task, agent: Agent) -> Dict: - """Execute a task on a specific agent""" - agent.current_tasks += 1 + """Execute a task on a specific agent with improved error handling""" + with SessionLocal() as db: + db_agent = db.query(ORMAgent).filter(ORMAgent.id == agent.id).first() + if db_agent: + db_agent.current_tasks += 1 + db.add(db_agent) + db.commit() + db.refresh(db_agent) + agent.current_tasks = db_agent.current_tasks # Update in-memory object + task.status = TaskStatus.IN_PROGRESS task.assigned_agent = agent.id @@ -146,18 +180,32 @@ Complete task → respond JSON format specified above.""" } try: - async with aiohttp.ClientSession() as session: - async with session.post(f"{agent.endpoint}/api/generate", json=payload) as response: - if response.status == 200: - result = await response.json() - task.result = result - task.status = TaskStatus.COMPLETED - task.completed_at = time.time() - print(f"Task {task.id} completed by {agent.id}") - return result - else: - raise Exception(f"HTTP {response.status}: {await response.text()}") + # Use the session initialized in the coordinator + session = getattr(self, 'session', None) + if not session: + raise Exception("HTTP session not initialized") + + async with session.post( + f"{agent.endpoint}/api/generate", + json=payload, + timeout=aiohttp.ClientTimeout(total=300) # 5 minute timeout for AI tasks + ) as response: + if response.status == 200: + result = await response.json() + task.result = result + task.status = TaskStatus.COMPLETED + task.completed_at = time.time() + print(f"Task {task.id} completed by {agent.id}") + return result + else: + error_text = await response.text() + raise Exception(f"HTTP {response.status}: {error_text}") + except asyncio.TimeoutError: + task.status = TaskStatus.FAILED + task.result = {"error": "Task execution timeout"} + print(f"Task {task.id} timed out on {agent.id}") + return {"error": "Task execution timeout"} except Exception as e: task.status = TaskStatus.FAILED task.result = {"error": str(e)} @@ -165,7 +213,14 @@ Complete task → respond JSON format specified above.""" return {"error": str(e)} finally: - agent.current_tasks -= 1 + with SessionLocal() as db: + db_agent = db.query(ORMAgent).filter(ORMAgent.id == agent.id).first() + if db_agent: + db_agent.current_tasks -= 1 + db.add(db_agent) + db.commit() + db.refresh(db_agent) + agent.current_tasks = db_agent.current_tasks # Update in-memory object async def process_queue(self): """Process the task queue with available agents""" @@ -246,8 +301,10 @@ Complete task → respond JSON format specified above.""" completion_rate = completed / total_tasks if total_tasks > 0 else 0 agent_vectors = {} - for agent in self.agents.values(): - agent_vectors[agent.id] = f"[{agent.specialty.value}@{agent.current_tasks}/{agent.max_concurrent}]" + with SessionLocal() as db: + db_agents = db.query(ORMAgent).all() + for agent in db_agents: + agent_vectors[agent.id] = f"[{agent.specialty}@{agent.current_tasks}/{agent.max_concurrent}]" return { "status_vector": status_vector, @@ -259,26 +316,94 @@ Complete task → respond JSON format specified above.""" "failed": failed, "in_progress": in_progress, "pending": total_tasks - completed - failed - in_progress, - "agents": {agent.id: agent.current_tasks for agent in self.agents.values()} + "agents": {agent.id: agent.current_tasks for agent in db_agents} } async def initialize(self): - """Initialize the coordinator""" - print("Initializing Hive Coordinator...") - self.is_initialized = True - print("✅ Hive Coordinator initialized") + """Initialize the coordinator with proper error handling""" + try: + print("Initializing Hive Coordinator...") + + # Initialize HTTP client session with timeouts + self.session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30, connect=10), + connector=aiohttp.TCPConnector( + limit=100, + limit_per_host=30, + ttl_dns_cache=300, + use_dns_cache=True + ) + ) + + # Initialize task processing + self.task_processor = None + + # Test connectivity to any configured agents + await self._test_initial_connectivity() + + self.is_initialized = True + print("✅ Hive Coordinator initialized") + + except Exception as e: + print(f"❌ Coordinator initialization failed: {e}") + self.is_initialized = False + # Clean up any partial initialization + if hasattr(self, 'session') and self.session: + await self.session.close() + raise async def shutdown(self): - """Shutdown the coordinator""" + """Enhanced shutdown with proper cleanup""" print("Shutting down Hive Coordinator...") - self.is_initialized = False - print("✅ Hive Coordinator shutdown") + + try: + # Cancel any running tasks + running_tasks = [task for task in self.tasks.values() if task.status == TaskStatus.IN_PROGRESS] + if running_tasks: + print(f"Canceling {len(running_tasks)} running tasks...") + for task in running_tasks: + task.status = TaskStatus.FAILED + task.result = {"error": "Coordinator shutdown"} + + # Close HTTP session + if hasattr(self, 'session') and self.session: + await self.session.close() + + # Stop task processor + if hasattr(self, 'task_processor') and self.task_processor: + self.task_processor.cancel() + try: + await self.task_processor + except asyncio.CancelledError: + pass + + self.is_initialized = False + print("✅ Hive Coordinator shutdown") + + except Exception as e: + print(f"❌ Shutdown error: {e}") + self.is_initialized = False + + async def _test_initial_connectivity(self): + """Test initial connectivity to prevent startup issues""" + # This would test any pre-configured agents + # For now, just ensure we can make HTTP requests + try: + async with self.session.get('http://httpbin.org/get', timeout=5) as response: + if response.status == 200: + print("✅ HTTP connectivity test passed") + except Exception as e: + print(f"⚠️ HTTP connectivity test failed: {e}") + # Don't fail initialization for this, just warn async def get_health_status(self): """Get health status""" + with SessionLocal() as db: + db_agents = db.query(ORMAgent).all() + agents_status = {agent.id: "available" for agent in db_agents} return { "status": "healthy" if self.is_initialized else "unhealthy", - "agents": {agent.id: "available" for agent in self.agents.values()}, + "agents": agents_status, "tasks": { "pending": len([t for t in self.tasks.values() if t.status == TaskStatus.PENDING]), "running": len([t for t in self.tasks.values() if t.status == TaskStatus.IN_PROGRESS]), @@ -289,6 +414,12 @@ Complete task → respond JSON format specified above.""" async def get_comprehensive_status(self): """Get comprehensive system status""" + with SessionLocal() as db: + db_agents = db.query(ORMAgent).all() + total_agents = len(db_agents) + available_agents = len([a for a in db_agents if a.current_tasks < a.max_concurrent]) + busy_agents = len([a for a in db_agents if a.current_tasks >= a.max_concurrent]) + return { "system": { "status": "operational" if self.is_initialized else "initializing", @@ -296,9 +427,19 @@ Complete task → respond JSON format specified above.""" "version": "1.0.0" }, "agents": { - "total": len(self.agents), - "available": len([a for a in self.agents.values() if a.current_tasks < a.max_concurrent]), - "busy": len([a for a in self.agents.values() if a.current_tasks >= a.max_concurrent]) + "total": total_agents, + "available": available_agents, + "busy": busy_agents, + "details": [ + { + "id": agent.id, + "endpoint": agent.endpoint, + "model": agent.model, + "specialty": agent.specialty, + "max_concurrent": agent.max_concurrent, + "current_tasks": agent.current_tasks + } for agent in db_agents + ] }, "tasks": { "total": len(self.tasks), @@ -313,9 +454,14 @@ Complete task → respond JSON format specified above.""" """Get Prometheus formatted metrics""" metrics = [] + with SessionLocal() as db: + db_agents = db.query(ORMAgent).all() + total_agents = len(db_agents) + available_agents = len([a for a in db_agents if a.current_tasks < a.max_concurrent]) + # Agent metrics - metrics.append(f"hive_agents_total {len(self.agents)}") - metrics.append(f"hive_agents_available {len([a for a in self.agents.values() if a.current_tasks < a.max_concurrent])}") + metrics.append(f"hive_agents_total {total_agents}") + metrics.append(f"hive_agents_available {available_agents}") # Task metrics metrics.append(f"hive_tasks_total {len(self.tasks)}") @@ -329,7 +475,7 @@ Complete task → respond JSON format specified above.""" # Example usage and testing functions async def demo_coordination(): """Demonstrate the coordination system""" - coordinator = AIDevCoordinator() + coordinator = HiveCoordinator() # Add example agents (you'll replace with your actual endpoints) coordinator.add_agent(Agent( diff --git a/mcp-server/SERVICE_README.md b/mcp-server/SERVICE_README.md new file mode 100644 index 00000000..7b19e163 --- /dev/null +++ b/mcp-server/SERVICE_README.md @@ -0,0 +1,133 @@ +# 🐝 Hive MCP Server Service + +This directory contains the systemd service configuration for running the Hive MCP Server as a background daemon with automatic agent discovery. + +## 🚀 Quick Start + +### 1. Install the Service +```bash +./install-service.sh +``` + +### 2. Start the Service +```bash +sudo systemctl start hive-mcp +``` + +### 3. Check Status +```bash +sudo systemctl status hive-mcp +``` + +### 4. View Logs +```bash +journalctl -u hive-mcp -f +``` + +## 🛠️ Management Script + +Use the provided management script for easy operations: + +```bash +# Install service +./hive-mcp.sh install + +# Start/stop/restart +./hive-mcp.sh start +./hive-mcp.sh stop +./hive-mcp.sh restart + +# Monitor +./hive-mcp.sh status +./hive-mcp.sh logs +./hive-mcp.sh follow + +# Agent management +./hive-mcp.sh discover # Trigger agent discovery +./hive-mcp.sh test # Test backend connection + +# Remove service +./hive-mcp.sh uninstall +``` + +## ⚙️ Configuration + +The service is configured via environment variables in the service file: + +- `HIVE_API_URL`: Hive backend API endpoint (default: https://hive.home.deepblack.cloud/api) +- `HIVE_WS_URL`: WebSocket endpoint (default: wss://hive.home.deepblack.cloud/socket.io) +- `AUTO_DISCOVERY`: Enable periodic discovery (default: true) +- `DISCOVERY_INTERVAL`: Discovery interval in ms (default: 300000 = 5 minutes) +- `LOG_LEVEL`: Logging level (default: info) + +## 🔄 Auto-Discovery + +The service automatically: + +1. **On Startup**: Scans the network for available Ollama agents +2. **Periodically**: Re-scans every 5 minutes (configurable) +3. **On Signal**: Triggers discovery when receiving SIGHUP (`systemctl reload hive-mcp`) + +## 📊 Monitoring + +### Service Status +```bash +sudo systemctl status hive-mcp +``` + +### Live Logs +```bash +journalctl -u hive-mcp -f +``` + +### Resource Usage +```bash +sudo systemctl show hive-mcp --property=MemoryCurrent,CPUUsageNSec +``` + +### Agent Status +```bash +curl -s https://hive.home.deepblack.cloud/api/agents | jq +``` + +## 🔧 Troubleshooting + +### Service Won't Start +1. Check logs: `journalctl -u hive-mcp -n 50` +2. Verify backend connectivity: `./hive-mcp.sh test` +3. Check file permissions: `ls -la /home/tony/AI/projects/hive/mcp-server/` + +### Auto-Discovery Issues +1. Check network connectivity to agent machines +2. Verify Ollama is running on target machines +3. Manually trigger discovery: `./hive-mcp.sh discover` + +### High Resource Usage +1. Check discovery interval: `grep DISCOVERY_INTERVAL /etc/systemd/system/hive-mcp.service` +2. Monitor agent count: `curl -s https://hive.home.deepblack.cloud/api/agents | jq '.total'` +3. Adjust memory limits in service file if needed + +## 🛡️ Security + +The service runs with: +- Non-root user (tony) +- Restricted filesystem access +- Memory and CPU limits +- Private tmp directory +- No new privileges + +## 📁 Files + +- `hive-mcp.service` - Systemd service definition +- `install-service.sh` - Service installation script +- `hive-mcp.sh` - Management script +- `logs/` - Log directory (created by service) +- `data/` - Data directory (created by service) + +## 🔗 Integration + +The service integrates with: +- **Hive Backend**: https://hive.home.deepblack.cloud/api +- **Socket.IO**: wss://hive.home.deepblack.cloud/socket.io +- **Systemd**: Full systemd service lifecycle +- **Journal**: Centralized logging via systemd-journald \ No newline at end of file diff --git a/mcp-server/dist/hive-client.js b/mcp-server/dist/hive-client.js index 0504f06b..5c30a847 100644 --- a/mcp-server/dist/hive-client.js +++ b/mcp-server/dist/hive-client.js @@ -11,8 +11,8 @@ export class HiveClient { wsConnection; constructor(config) { this.config = { - baseUrl: process.env.HIVE_API_URL || 'https://hive.home.deepblack.cloud', - wsUrl: process.env.HIVE_WS_URL || 'wss://hive.home.deepblack.cloud', + baseUrl: process.env.HIVE_API_URL || 'https://hive.home.deepblack.cloud/api', + wsUrl: process.env.HIVE_WS_URL || 'wss://hive.home.deepblack.cloud/socket.io', timeout: parseInt(process.env.HIVE_TIMEOUT || '30000'), ...config, }; @@ -27,7 +27,7 @@ export class HiveClient { async testConnection() { try { const response = await this.api.get('/health'); - return response.data.status === 'healthy'; + return response.data.status === 'healthy' || response.status === 200; } catch (error) { throw new Error(`Failed to connect to Hive: ${error}`); diff --git a/mcp-server/dist/hive-client.js.map b/mcp-server/dist/hive-client.js.map index 4deb40b6..029e5cda 100644 --- a/mcp-server/dist/hive-client.js.map +++ b/mcp-server/dist/hive-client.js.map @@ -1 +1 @@ -{"version":3,"file":"hive-client.js","sourceRoot":"","sources":["../src/hive-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"hive-client.js","sourceRoot":"","sources":["../src/hive-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp-server/dist/index.js b/mcp-server/dist/index.js index 4b481b9d..2532b799 100644 --- a/mcp-server/dist/index.js +++ b/mcp-server/dist/index.js @@ -16,6 +16,8 @@ class HiveMCPServer { hiveClient; hiveTools; hiveResources; + discoveryInterval; + isDaemonMode = false; constructor() { this.server = new Server({ name: 'hive-mcp-server', @@ -58,12 +60,23 @@ class HiveMCPServer { console.error('[MCP Server Error]:', error); }; process.on('SIGINT', async () => { - await this.server.close(); - process.exit(0); + await this.shutdown(); + }); + process.on('SIGTERM', async () => { + await this.shutdown(); + }); + process.on('SIGHUP', async () => { + console.log('🔄 Received SIGHUP, triggering agent discovery...'); + await this.autoDiscoverAgents(); }); } async start() { console.log('🐝 Starting Hive MCP Server...'); + // Check for daemon mode + this.isDaemonMode = process.argv.includes('--daemon'); + if (this.isDaemonMode) { + console.log('🔧 Running in daemon mode'); + } // Test connection to Hive backend try { await this.hiveClient.testConnection(); @@ -82,10 +95,38 @@ class HiveMCPServer { catch (error) { console.warn('⚠️ Auto-discovery failed, continuing without it:', error); } - const transport = new StdioServerTransport(); - await this.server.connect(transport); - console.log('🚀 Hive MCP Server running on stdio'); - console.log('🔗 AI assistants can now orchestrate your distributed cluster!'); + // Set up periodic auto-discovery if enabled + if (this.isDaemonMode && process.env.AUTO_DISCOVERY !== 'false') { + this.setupPeriodicDiscovery(); + } + if (this.isDaemonMode) { + console.log('🚀 Hive MCP Server running in daemon mode'); + console.log('🔗 Monitoring cluster and auto-discovering agents...'); + // Keep the process alive in daemon mode + setInterval(() => { + // Health check - could add cluster monitoring here + }, 30000); + } + else { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.log('🚀 Hive MCP Server running on stdio'); + console.log('🔗 AI assistants can now orchestrate your distributed cluster!'); + } + } + setupPeriodicDiscovery() { + const interval = parseInt(process.env.DISCOVERY_INTERVAL || '300000', 10); // Default 5 minutes + console.log(`🔄 Setting up periodic auto-discovery every ${interval / 1000} seconds`); + this.discoveryInterval = setInterval(async () => { + console.log('🔍 Periodic agent auto-discovery...'); + try { + await this.autoDiscoverAgents(); + console.log('✅ Periodic auto-discovery completed'); + } + catch (error) { + console.warn('⚠️ Periodic auto-discovery failed:', error); + } + }, interval); } async autoDiscoverAgents() { // Use the existing hive_bring_online functionality @@ -97,6 +138,16 @@ class HiveMCPServer { throw new Error(`Auto-discovery failed: ${result.content[0]?.text || 'Unknown error'}`); } } + async shutdown() { + console.log('🛑 Shutting down Hive MCP Server...'); + if (this.discoveryInterval) { + clearInterval(this.discoveryInterval); + console.log('✅ Stopped periodic auto-discovery'); + } + await this.server.close(); + console.log('✅ Hive MCP Server stopped'); + process.exit(0); + } } // Start the server const server = new HiveMCPServer(); diff --git a/mcp-server/dist/index.js.map b/mcp-server/dist/index.js.map index 3ae4e04c..3f528f02 100644 --- a/mcp-server/dist/index.js.map +++ b/mcp-server/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,aAAa;IACT,MAAM,CAAS;IACf,UAAU,CAAa;IACvB,SAAS,CAAY;IACrB,aAAa,CAAgB;IAC7B,iBAAiB,CAAkB;IACnC,YAAY,GAAY,KAAK,CAAC;IAEtC;QACE,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CACtB;YACE,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,OAAO;SACjB,EACD;YACE,YAAY,EAAE;gBACZ,KAAK,EAAE,EAAE;gBACT,SAAS,EAAE,EAAE;aACd;SACF,CACF,CAAC;QAEF,sCAAsC;QACtC,IAAI,CAAC,UAAU,GAAG,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,aAAa,GAAG,IAAI,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAExD,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAEO,aAAa;QACnB,uDAAuD;QACvD,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE;YAC/D,OAAO;gBACL,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;aACpC,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACrE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;YACjD,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;QAEH,kEAAkE;QAClE,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACnE,OAAO;gBACL,SAAS,EAAE,MAAM,IAAI,CAAC,aAAa,CAAC,eAAe,EAAE;aACtD,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,yBAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACzE,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;YAC/B,OAAO,MAAM,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,iBAAiB;QACjB,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE;YAC9B,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;QAC9C,CAAC,CAAC;QAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YAC9B,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;YAC/B,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YAC9B,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;YACjE,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAK;QACT,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAE9C,wBAAwB;QACxB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACtD,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QAC3C,CAAC;QAED,kCAAkC;QAClC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,+CAA+C;QAC/C,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,mDAAmD,EAAE,KAAK,CAAC,CAAC;QAC3E,CAAC;QAED,4CAA4C;QAC5C,IAAI,IAAI,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,OAAO,EAAE,CAAC;YAChE,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAChC,CAAC;QAED,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;YACzD,OAAO,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAC;YAEpE,wCAAwC;YACxC,WAAW,CAAC,GAAG,EAAE;gBACf,mDAAmD;YACrD,CAAC,EAAE,KAAK,CAAC,CAAC;QACZ,CAAC;aAAM,CAAC;YACN,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;YAC7C,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAErC,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;YACnD,OAAO,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IAEO,sBAAsB;QAC5B,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,oBAAoB;QAC/F,OAAO,CAAC,GAAG,CAAC,+CAA+C,QAAQ,GAAG,IAAI,UAAU,CAAC,CAAC;QAEtF,IAAI,CAAC,iBAAiB,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;YAC9C,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;YACnD,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAChC,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,EAAE,QAAQ,CAAC,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,kBAAkB;QAC9B,mDAAmD;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,mBAAmB,EAAE;YACnE,aAAa,EAAE,KAAK;YACpB,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,eAAe,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;QAEnD,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3B,aAAa,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;CACF;AAED,mBAAmB;AACnB,MAAM,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;AACnC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7B,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/mcp-server/hive-mcp.service b/mcp-server/hive-mcp.service new file mode 100644 index 00000000..289a3a5a --- /dev/null +++ b/mcp-server/hive-mcp.service @@ -0,0 +1,52 @@ +[Unit] +Description=Hive MCP Server - Distributed AI Orchestration +Documentation=https://github.com/anthropics/hive-mcp-server +After=network-online.target +Wants=network-online.target +StartLimitIntervalSec=30 +StartLimitBurst=3 + +[Service] +Type=simple +User=tony +Group=tony +WorkingDirectory=/home/tony/AI/projects/hive/mcp-server + +# Environment variables +Environment=NODE_ENV=production +Environment=HIVE_API_URL=https://hive.home.deepblack.cloud/api +Environment=HIVE_WS_URL=wss://hive.home.deepblack.cloud/socket.io +Environment=LOG_LEVEL=info +Environment=AUTO_DISCOVERY=true +Environment=DISCOVERY_INTERVAL=300000 + +# Main service command +ExecStart=/usr/bin/node dist/index.js --daemon +ExecReload=/bin/kill -HUP $MAINPID + +# Restart policy +Restart=always +RestartSec=10 +TimeoutStartSec=30 +TimeoutStopSec=15 + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=/home/tony/AI/projects/hive/mcp-server/logs +ReadWritePaths=/home/tony/AI/projects/hive/mcp-server/data + +# Resource limits +LimitNOFILE=65536 +MemoryMax=512M +CPUQuota=50% + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=hive-mcp + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/mcp-server/hive-mcp.sh b/mcp-server/hive-mcp.sh new file mode 100755 index 00000000..253c7be0 --- /dev/null +++ b/mcp-server/hive-mcp.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# Hive MCP Server Management Script + +set -e + +SERVICE_NAME="hive-mcp" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +function log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +function success() { + echo -e "${GREEN}✅ $1${NC}" +} + +function warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +function error() { + echo -e "${RED}❌ $1${NC}" +} + +function show_status() { + log "Checking Hive MCP Server status..." + sudo systemctl status $SERVICE_NAME --no-pager +} + +function start_service() { + log "Starting Hive MCP Server..." + sudo systemctl start $SERVICE_NAME + sleep 2 + if sudo systemctl is-active --quiet $SERVICE_NAME; then + success "Hive MCP Server started successfully" + else + error "Failed to start Hive MCP Server" + show_logs + exit 1 + fi +} + +function stop_service() { + log "Stopping Hive MCP Server..." + sudo systemctl stop $SERVICE_NAME + success "Hive MCP Server stopped" +} + +function restart_service() { + log "Restarting Hive MCP Server..." + sudo systemctl restart $SERVICE_NAME + sleep 2 + if sudo systemctl is-active --quiet $SERVICE_NAME; then + success "Hive MCP Server restarted successfully" + else + error "Failed to restart Hive MCP Server" + show_logs + exit 1 + fi +} + +function reload_service() { + log "Triggering agent discovery (SIGHUP)..." + sudo systemctl reload $SERVICE_NAME + success "Agent discovery triggered" +} + +function show_logs() { + log "Showing recent logs..." + journalctl -u $SERVICE_NAME --no-pager -n 50 +} + +function follow_logs() { + log "Following live logs (Ctrl+C to exit)..." + journalctl -u $SERVICE_NAME -f +} + +function test_connection() { + log "Testing connection to Hive backend..." + cd "$SCRIPT_DIR" + if node test-mcp.cjs > /dev/null 2>&1; then + success "Connection test passed" + else + error "Connection test failed" + log "Running detailed test..." + node test-mcp.cjs + fi +} + +function discover_agents() { + log "Manually triggering agent discovery..." + reload_service + sleep 3 + log "Current registered agents:" + curl -s https://hive.home.deepblack.cloud/api/agents | jq '.agents[] | {id: .id, model: .model, specialty: .specialty}' 2>/dev/null || { + warning "Could not fetch agent list - API may be unreachable" + } +} + +function install_service() { + if [ -f "$SCRIPT_DIR/install-service.sh" ]; then + log "Running service installation..." + cd "$SCRIPT_DIR" + ./install-service.sh + else + error "Installation script not found" + exit 1 + fi +} + +function uninstall_service() { + log "Uninstalling Hive MCP Server service..." + sudo systemctl stop $SERVICE_NAME 2>/dev/null || true + sudo systemctl disable $SERVICE_NAME 2>/dev/null || true + sudo rm -f /etc/systemd/system/$SERVICE_NAME.service + sudo systemctl daemon-reload + success "Hive MCP Server service uninstalled" +} + +function show_help() { + echo "🐝 Hive MCP Server Management Script" + echo "" + echo "Usage: $0 [COMMAND]" + echo "" + echo "Commands:" + echo " install Install the systemd service" + echo " uninstall Remove the systemd service" + echo " start Start the service" + echo " stop Stop the service" + echo " restart Restart the service" + echo " reload Trigger agent discovery (SIGHUP)" + echo " status Show service status" + echo " logs Show recent logs" + echo " follow Follow live logs" + echo " test Test connection to Hive backend" + echo " discover Manually trigger agent discovery" + echo " help Show this help message" + echo "" + echo "Examples:" + echo " $0 start # Start the service" + echo " $0 status # Check if running" + echo " $0 discover # Find new agents" + echo " $0 follow # Watch logs in real-time" +} + +# Main command handling +case "${1:-help}" in + install) + install_service + ;; + uninstall) + uninstall_service + ;; + start) + start_service + ;; + stop) + stop_service + ;; + restart) + restart_service + ;; + reload) + reload_service + ;; + status) + show_status + ;; + logs) + show_logs + ;; + follow) + follow_logs + ;; + test) + test_connection + ;; + discover) + discover_agents + ;; + help|--help|-h) + show_help + ;; + *) + error "Unknown command: $1" + echo "" + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/mcp-server/install-service.sh b/mcp-server/install-service.sh new file mode 100755 index 00000000..ce4816b4 --- /dev/null +++ b/mcp-server/install-service.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Hive MCP Server Service Installation Script + +set -e + +echo "🐝 Installing Hive MCP Server as a systemd service..." + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + echo "❌ This script should not be run as root. Run as the user who will own the service." + exit 1 +fi + +# Verify the service file exists +if [ ! -f "hive-mcp.service" ]; then + echo "❌ Service file 'hive-mcp.service' not found in current directory" + exit 1 +fi + +# Verify the built application exists +if [ ! -f "dist/index.js" ]; then + echo "❌ Built application not found. Run 'npm run build' first." + exit 1 +fi + +# Create log and data directories with proper permissions +echo "📁 Creating directories..." +mkdir -p logs data +chmod 755 logs data + +# Copy service file to systemd directory +echo "📄 Installing service file..." +sudo cp hive-mcp.service /etc/systemd/system/ + +# Reload systemd daemon +echo "🔄 Reloading systemd daemon..." +sudo systemctl daemon-reload + +# Enable the service +echo "✅ Enabling Hive MCP service..." +sudo systemctl enable hive-mcp.service + +echo "" +echo "🎉 Hive MCP Server service installed successfully!" +echo "" +echo "📋 Available commands:" +echo " sudo systemctl start hive-mcp # Start the service" +echo " sudo systemctl stop hive-mcp # Stop the service" +echo " sudo systemctl restart hive-mcp # Restart the service" +echo " sudo systemctl status hive-mcp # Check service status" +echo " sudo systemctl disable hive-mcp # Disable auto-start" +echo " journalctl -u hive-mcp -f # View live logs" +echo " sudo systemctl reload hive-mcp # Trigger agent discovery" +echo "" +echo "🚀 To start the service now, run:" +echo " sudo systemctl start hive-mcp" +echo "" +echo "📊 To check the status, run:" +echo " sudo systemctl status hive-mcp" \ No newline at end of file diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 8e27e175..23e98255 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -24,6 +24,8 @@ class HiveMCPServer { private hiveClient: HiveClient; private hiveTools: HiveTools; private hiveResources: HiveResources; + private discoveryInterval?: NodeJS.Timeout; + private isDaemonMode: boolean = false; constructor() { this.server = new Server( @@ -78,14 +80,28 @@ class HiveMCPServer { }; process.on('SIGINT', async () => { - await this.server.close(); - process.exit(0); + await this.shutdown(); + }); + + process.on('SIGTERM', async () => { + await this.shutdown(); + }); + + process.on('SIGHUP', async () => { + console.log('🔄 Received SIGHUP, triggering agent discovery...'); + await this.autoDiscoverAgents(); }); } async start() { console.log('🐝 Starting Hive MCP Server...'); + // Check for daemon mode + this.isDaemonMode = process.argv.includes('--daemon'); + if (this.isDaemonMode) { + console.log('🔧 Running in daemon mode'); + } + // Test connection to Hive backend try { await this.hiveClient.testConnection(); @@ -104,11 +120,41 @@ class HiveMCPServer { console.warn('⚠️ Auto-discovery failed, continuing without it:', error); } - const transport = new StdioServerTransport(); - await this.server.connect(transport); + // Set up periodic auto-discovery if enabled + if (this.isDaemonMode && process.env.AUTO_DISCOVERY !== 'false') { + this.setupPeriodicDiscovery(); + } + + if (this.isDaemonMode) { + console.log('🚀 Hive MCP Server running in daemon mode'); + console.log('🔗 Monitoring cluster and auto-discovering agents...'); + + // Keep the process alive in daemon mode + setInterval(() => { + // Health check - could add cluster monitoring here + }, 30000); + } else { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + + console.log('🚀 Hive MCP Server running on stdio'); + console.log('🔗 AI assistants can now orchestrate your distributed cluster!'); + } + } + + private setupPeriodicDiscovery() { + const interval = parseInt(process.env.DISCOVERY_INTERVAL || '300000', 10); // Default 5 minutes + console.log(`🔄 Setting up periodic auto-discovery every ${interval / 1000} seconds`); - console.log('🚀 Hive MCP Server running on stdio'); - console.log('🔗 AI assistants can now orchestrate your distributed cluster!'); + this.discoveryInterval = setInterval(async () => { + console.log('🔍 Periodic agent auto-discovery...'); + try { + await this.autoDiscoverAgents(); + console.log('✅ Periodic auto-discovery completed'); + } catch (error) { + console.warn('⚠️ Periodic auto-discovery failed:', error); + } + }, interval); } private async autoDiscoverAgents() { @@ -122,6 +168,19 @@ class HiveMCPServer { throw new Error(`Auto-discovery failed: ${result.content[0]?.text || 'Unknown error'}`); } } + + private async shutdown() { + console.log('🛑 Shutting down Hive MCP Server...'); + + if (this.discoveryInterval) { + clearInterval(this.discoveryInterval); + console.log('✅ Stopped periodic auto-discovery'); + } + + await this.server.close(); + console.log('✅ Hive MCP Server stopped'); + process.exit(0); + } } // Start the server diff --git a/mcp-server/test-mcp.cjs b/mcp-server/test-mcp.cjs new file mode 100755 index 00000000..54393149 --- /dev/null +++ b/mcp-server/test-mcp.cjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +/** + * Simple MCP Server Test Suite + * Tests the core functionality of the Hive MCP server + */ + +const { spawn } = require('child_process'); +const https = require('https'); + +// Test configuration +const API_BASE = 'https://hive.home.deepblack.cloud/api'; +const TEST_TIMEOUT = 30000; + +// Colors for output +const colors = { + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + reset: '\x1b[0m' +}; + +function log(message, color = colors.reset) { + console.log(`${color}${message}${colors.reset}`); +} + +// Test cases +const tests = [ + { + name: 'API Health Check', + test: () => testApiHealth() + }, + { + name: 'Agent List', + test: () => testAgentList() + }, + { + name: 'MCP Server Connectivity', + test: () => testMcpServer() + }, + { + name: 'Socket.IO Endpoint', + test: () => testSocketIO() + } +]; + +async function testApiHealth() { + return new Promise((resolve, reject) => { + https.get(`${API_BASE}/health`, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + if (parsed.status === 'healthy') { + resolve(`✅ API healthy, ${Object.keys(parsed.components.agents).length} agents`); + } else { + reject('API not healthy'); + } + } catch (e) { + reject('Invalid JSON response'); + } + }); + }).on('error', reject); + }); +} + +async function testAgentList() { + return new Promise((resolve, reject) => { + https.get(`${API_BASE}/agents`, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + if (parsed.agents && Array.isArray(parsed.agents)) { + resolve(`✅ ${parsed.total} agents registered`); + } else { + reject('Invalid agents response'); + } + } catch (e) { + reject('Invalid JSON response'); + } + }); + }).on('error', reject); + }); +} + +async function testMcpServer() { + return new Promise((resolve, reject) => { + const mcpProcess = spawn('node', ['dist/index.js'], { + cwd: '/home/tony/AI/projects/hive/mcp-server', + stdio: 'pipe' + }); + + let output = ''; + let resolved = false; + + mcpProcess.stdout.on('data', (data) => { + output += data.toString(); + if (output.includes('Connected to Hive backend successfully') && !resolved) { + resolved = true; + mcpProcess.kill(); + resolve('✅ MCP server connects successfully'); + } + }); + + mcpProcess.stderr.on('data', (data) => { + if (!resolved) { + resolved = true; + mcpProcess.kill(); + reject(`MCP server error: ${data.toString()}`); + } + }); + + setTimeout(() => { + if (!resolved) { + resolved = true; + mcpProcess.kill(); + reject('MCP server timeout'); + } + }, 10000); + }); +} + +async function testSocketIO() { + return new Promise((resolve, reject) => { + const url = 'https://hive.home.deepblack.cloud/socket.io/?EIO=4&transport=polling'; + https.get(url, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (data.includes('sid') && data.includes('upgrades')) { + resolve('✅ Socket.IO endpoint responding'); + } else { + reject('Socket.IO endpoint not responding properly'); + } + }); + }).on('error', reject); + }); +} + +// Main test runner +async function runTests() { + log('\n🐝 Hive MCP Server Test Suite\n', colors.blue); + + let passed = 0; + let failed = 0; + + for (const test of tests) { + try { + log(`Testing: ${test.name}...`, colors.yellow); + const result = await test.test(); + log(` ${result}`, colors.green); + passed++; + } catch (error) { + log(` ❌ ${error}`, colors.red); + failed++; + } + } + + log(`\n📊 Test Results:`, colors.blue); + log(` Passed: ${passed}`, colors.green); + log(` Failed: ${failed}`, failed > 0 ? colors.red : colors.green); + log(` Total: ${passed + failed}`, colors.blue); + + if (failed === 0) { + log('\n🎉 All tests passed! Hive MCP system is operational.', colors.green); + } else { + log('\n⚠️ Some tests failed. Check the errors above.', colors.yellow); + } + + process.exit(failed > 0 ? 1 : 0); +} + +// Run tests +runTests().catch(console.error); \ No newline at end of file