Compare commits
	
		
			2 Commits
		
	
	
		
			ef3b61740b
			...
			feature/ag
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1e81daaf18 | ||
|   | 9262e63374 | 
							
								
								
									
										294
									
								
								backend/app/api/repository.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								backend/app/api/repository.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| """ | ||||
| Repository management API endpoints | ||||
| """ | ||||
|  | ||||
| from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks | ||||
| from sqlalchemy.orm import Session | ||||
| from typing import List, Dict, Any, Optional | ||||
| from datetime import datetime | ||||
|  | ||||
| from ..core.database import get_db | ||||
| from ..models.project import Project | ||||
| from ..services.repository_service import repository_service | ||||
| from ..auth.auth import get_current_user | ||||
|  | ||||
| router = APIRouter() | ||||
|  | ||||
| @router.get("/repositories", response_model=List[Dict[str, Any]]) | ||||
| async def list_repositories( | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """List all repositories with bzzz integration enabled""" | ||||
|     try: | ||||
|         projects = db.query(Project).filter( | ||||
|             Project.bzzz_enabled == True | ||||
|         ).all() | ||||
|          | ||||
|         repositories = [] | ||||
|         for project in projects: | ||||
|             repo_data = { | ||||
|                 "id": project.id, | ||||
|                 "name": project.name, | ||||
|                 "description": project.description, | ||||
|                 "provider": project.provider or "github", | ||||
|                 "provider_base_url": project.provider_base_url, | ||||
|                 "owner": project.git_owner, | ||||
|                 "repository": project.git_repository, | ||||
|                 "branch": project.git_branch, | ||||
|                 "status": project.status, | ||||
|                 "bzzz_enabled": project.bzzz_enabled, | ||||
|                 "ready_to_claim": project.ready_to_claim, | ||||
|                 "auto_assignment": getattr(project, "auto_assignment", True), | ||||
|                 "created_at": project.created_at.isoformat() if project.created_at else None | ||||
|             } | ||||
|             repositories.append(repo_data) | ||||
|          | ||||
|         return repositories | ||||
|          | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to list repositories: {str(e)}") | ||||
|  | ||||
| @router.post("/repositories/sync") | ||||
| async def sync_repositories( | ||||
|     background_tasks: BackgroundTasks, | ||||
|     repository_ids: Optional[List[int]] = None, | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Sync tasks from repositories""" | ||||
|     try: | ||||
|         if repository_ids: | ||||
|             # Sync specific repositories | ||||
|             projects = db.query(Project).filter( | ||||
|                 Project.id.in_(repository_ids), | ||||
|                 Project.bzzz_enabled == True | ||||
|             ).all() | ||||
|              | ||||
|             if not projects: | ||||
|                 raise HTTPException(status_code=404, detail="No matching repositories found") | ||||
|              | ||||
|             results = {"synced_projects": 0, "new_tasks": 0, "assigned_tasks": 0, "errors": []} | ||||
|              | ||||
|             for project in projects: | ||||
|                 try: | ||||
|                     sync_result = await repository_service.sync_project_tasks(db, project) | ||||
|                     results["synced_projects"] += 1 | ||||
|                     results["new_tasks"] += sync_result.get("new_tasks", 0) | ||||
|                     results["assigned_tasks"] += sync_result.get("assigned_tasks", 0) | ||||
|                 except Exception as e: | ||||
|                     results["errors"].append(f"Project {project.name}: {str(e)}") | ||||
|              | ||||
|             return results | ||||
|         else: | ||||
|             # Sync all repositories in background | ||||
|             background_tasks.add_task(repository_service.sync_all_repositories, db) | ||||
|             return {"message": "Repository sync started in background", "status": "initiated"} | ||||
|              | ||||
|     except HTTPException: | ||||
|         raise | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to sync repositories: {str(e)}") | ||||
|  | ||||
| @router.get("/repositories/{repository_id}/stats") | ||||
| async def get_repository_stats( | ||||
|     repository_id: int, | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Get task statistics for a specific repository""" | ||||
|     try: | ||||
|         stats = await repository_service.get_project_task_stats(db, repository_id) | ||||
|         return stats | ||||
|          | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to get repository stats: {str(e)}") | ||||
|  | ||||
| @router.post("/repositories/{repository_id}/sync") | ||||
| async def sync_repository( | ||||
|     repository_id: int, | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Sync tasks from a specific repository""" | ||||
|     try: | ||||
|         project = db.query(Project).filter( | ||||
|             Project.id == repository_id, | ||||
|             Project.bzzz_enabled == True | ||||
|         ).first() | ||||
|          | ||||
|         if not project: | ||||
|             raise HTTPException(status_code=404, detail="Repository not found or bzzz integration not enabled") | ||||
|          | ||||
|         result = await repository_service.sync_project_tasks(db, project) | ||||
|         return result | ||||
|          | ||||
|     except HTTPException: | ||||
|         raise | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to sync repository: {str(e)}") | ||||
|  | ||||
| @router.put("/repositories/{repository_id}/config") | ||||
| async def update_repository_config( | ||||
|     repository_id: int, | ||||
|     config_data: Dict[str, Any], | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Update repository configuration""" | ||||
|     try: | ||||
|         project = db.query(Project).filter(Project.id == repository_id).first() | ||||
|          | ||||
|         if not project: | ||||
|             raise HTTPException(status_code=404, detail="Repository not found") | ||||
|          | ||||
|         # Update allowed configuration fields | ||||
|         if "auto_assignment" in config_data: | ||||
|             setattr(project, "auto_assignment", config_data["auto_assignment"]) | ||||
|          | ||||
|         if "bzzz_enabled" in config_data: | ||||
|             project.bzzz_enabled = config_data["bzzz_enabled"] | ||||
|          | ||||
|         if "ready_to_claim" in config_data: | ||||
|             project.ready_to_claim = config_data["ready_to_claim"] | ||||
|          | ||||
|         if "status" in config_data and config_data["status"] in ["active", "inactive", "archived"]: | ||||
|             project.status = config_data["status"] | ||||
|          | ||||
|         db.commit() | ||||
|          | ||||
|         return {"message": "Repository configuration updated", "repository_id": repository_id} | ||||
|          | ||||
|     except HTTPException: | ||||
|         raise | ||||
|     except Exception as e: | ||||
|         db.rollback() | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to update repository config: {str(e)}") | ||||
|  | ||||
| @router.get("/repositories/{repository_id}/tasks") | ||||
| async def get_repository_tasks( | ||||
|     repository_id: int, | ||||
|     limit: int = 50, | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Get available tasks from a repository""" | ||||
|     try: | ||||
|         project = db.query(Project).filter( | ||||
|             Project.id == repository_id, | ||||
|             Project.bzzz_enabled == True | ||||
|         ).first() | ||||
|          | ||||
|         if not project: | ||||
|             raise HTTPException(status_code=404, detail="Repository not found or bzzz integration not enabled") | ||||
|          | ||||
|         # Get repository client and fetch tasks | ||||
|         repo_client = await repository_service._get_repository_client(project) | ||||
|         if not repo_client: | ||||
|             raise HTTPException(status_code=500, detail="Failed to create repository client") | ||||
|          | ||||
|         tasks = await repo_client.list_available_tasks() | ||||
|          | ||||
|         # Limit results | ||||
|         if len(tasks) > limit: | ||||
|             tasks = tasks[:limit] | ||||
|          | ||||
|         return { | ||||
|             "repository_id": repository_id, | ||||
|             "repository_name": project.name, | ||||
|             "provider": project.provider or "github", | ||||
|             "tasks": tasks, | ||||
|             "total_tasks": len(tasks) | ||||
|         } | ||||
|          | ||||
|     except HTTPException: | ||||
|         raise | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to get repository tasks: {str(e)}") | ||||
|  | ||||
| @router.post("/repositories/discover") | ||||
| async def discover_repositories( | ||||
|     provider: str = "gitea", | ||||
|     base_url: str = "http://192.168.1.113:3000", | ||||
|     db: Session = Depends(get_db), | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Discover repositories from a provider (placeholder for future implementation)""" | ||||
|     try: | ||||
|         # This would implement repository discovery functionality | ||||
|         # For now, return the manually configured repositories | ||||
|          | ||||
|         existing_repos = db.query(Project).filter( | ||||
|             Project.provider == provider, | ||||
|             Project.provider_base_url == base_url | ||||
|         ).all() | ||||
|          | ||||
|         discovered = [] | ||||
|         for repo in existing_repos: | ||||
|             discovered.append({ | ||||
|                 "name": repo.name, | ||||
|                 "owner": repo.git_owner, | ||||
|                 "repository": repo.git_repository, | ||||
|                 "description": repo.description, | ||||
|                 "already_configured": True | ||||
|             }) | ||||
|          | ||||
|         return { | ||||
|             "provider": provider, | ||||
|             "base_url": base_url, | ||||
|             "discovered_repositories": discovered | ||||
|         } | ||||
|          | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to discover repositories: {str(e)}") | ||||
|  | ||||
| @router.post("/webhook/{repository_id}") | ||||
| async def handle_repository_webhook( | ||||
|     repository_id: int, | ||||
|     payload: Dict[str, Any], | ||||
|     db: Session = Depends(get_db) | ||||
| ): | ||||
|     """Handle webhook events from repositories""" | ||||
|     try: | ||||
|         project = db.query(Project).filter(Project.id == repository_id).first() | ||||
|          | ||||
|         if not project: | ||||
|             raise HTTPException(status_code=404, detail="Repository not found") | ||||
|          | ||||
|         # Log the webhook event (would be stored in webhook_events table) | ||||
|         event_type = payload.get("action", "unknown") | ||||
|          | ||||
|         # For now, just trigger a sync if it's an issue event | ||||
|         if "issue" in payload and event_type in ["opened", "labeled", "unlabeled"]: | ||||
|             # Check if it's a bzzz-task | ||||
|             issue = payload.get("issue", {}) | ||||
|             labels = [label["name"] for label in issue.get("labels", [])] | ||||
|              | ||||
|             if "bzzz-task" in labels: | ||||
|                 # Trigger task sync for this project | ||||
|                 await repository_service.sync_project_tasks(db, project) | ||||
|                  | ||||
|                 return { | ||||
|                     "message": "Webhook processed, task sync triggered", | ||||
|                     "event_type": event_type, | ||||
|                     "issue_number": issue.get("number") | ||||
|                 } | ||||
|          | ||||
|         return {"message": "Webhook received", "event_type": event_type} | ||||
|          | ||||
|     except HTTPException: | ||||
|         raise | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to process webhook: {str(e)}") | ||||
|  | ||||
| @router.delete("/repositories/cache") | ||||
| async def clear_task_cache( | ||||
|     current_user: dict = Depends(get_current_user) | ||||
| ): | ||||
|     """Clear the task cache""" | ||||
|     try: | ||||
|         await repository_service.cleanup_old_cache(max_age_hours=0)  # Clear all | ||||
|         return {"message": "Task cache cleared"} | ||||
|          | ||||
|     except Exception as e: | ||||
|         raise HTTPException(status_code=500, detail=f"Failed to clear cache: {str(e)}") | ||||
| @@ -1,4 +1,5 @@ | ||||
| from . import agent | ||||
| from . import agent_role | ||||
| from . import project | ||||
| from . import task | ||||
| from . import sqlalchemy_models | ||||
| @@ -1,4 +1,4 @@ | ||||
| from sqlalchemy import Column, Integer, String, DateTime, JSON | ||||
| from sqlalchemy import Column, Integer, String, DateTime, JSON, Text | ||||
| from sqlalchemy.sql import func | ||||
| from sqlalchemy.orm import relationship | ||||
| from ..core.database import Base | ||||
| @@ -24,6 +24,14 @@ class Agent(Base): | ||||
|     updated_at = Column(DateTime(timezone=True), onupdate=func.now()) | ||||
|     last_seen = Column(DateTime(timezone=True), nullable=True) | ||||
|      | ||||
|     # Role-based collaboration fields | ||||
|     role = Column(String, nullable=True)  # Role from Bees-AgenticWorkers | ||||
|     system_prompt = Column(Text, nullable=True)  # Role-specific system prompt | ||||
|     reports_to = Column(JSON, nullable=True)  # Array of roles this agent reports to | ||||
|     expertise = Column(JSON, nullable=True)  # Array of expertise areas | ||||
|     deliverables = Column(JSON, nullable=True)  # Array of deliverables | ||||
|     collaboration_settings = Column(JSON, nullable=True)  # Collaboration preferences | ||||
|      | ||||
|     # Relationships | ||||
|     tasks = relationship("Task", back_populates="assigned_agent") | ||||
|      | ||||
| @@ -45,5 +53,13 @@ class Agent(Base): | ||||
|             "performance_targets": self.performance_targets, | ||||
|             "created_at": self.created_at.isoformat() if self.created_at else None, | ||||
|             "updated_at": self.updated_at.isoformat() if self.updated_at else None, | ||||
|             "last_seen": self.last_seen.isoformat() if self.last_seen else None | ||||
|             "last_seen": self.last_seen.isoformat() if self.last_seen else None, | ||||
|              | ||||
|             # Role-based fields | ||||
|             "role": self.role, | ||||
|             "system_prompt": self.system_prompt, | ||||
|             "reports_to": self.reports_to, | ||||
|             "expertise": self.expertise, | ||||
|             "deliverables": self.deliverables, | ||||
|             "collaboration_settings": self.collaboration_settings | ||||
|         } | ||||
							
								
								
									
										69
									
								
								backend/app/models/agent_role.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								backend/app/models/agent_role.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| from sqlalchemy import Column, String, DateTime, JSON, Text | ||||
| from sqlalchemy.sql import func | ||||
| from sqlalchemy.orm import relationship | ||||
| from ..core.database import Base | ||||
|  | ||||
| class AgentRole(Base): | ||||
|     __tablename__ = "agent_roles" | ||||
|  | ||||
|     id = Column(String, primary_key=True, index=True) | ||||
|     name = Column(String, unique=True, nullable=False, index=True)  # Role identifier (e.g., "senior_software_architect") | ||||
|     display_name = Column(String, nullable=False)  # Human-readable name | ||||
|     system_prompt = Column(Text, nullable=False)  # Role-specific system prompt | ||||
|     reports_to = Column(JSON, nullable=True)  # Array of roles this role reports to | ||||
|     expertise = Column(JSON, nullable=True)  # Array of expertise areas | ||||
|     deliverables = Column(JSON, nullable=True)  # Array of deliverables | ||||
|     capabilities = Column(JSON, nullable=True)  # Array of capabilities | ||||
|     collaboration_defaults = Column(JSON, nullable=True)  # Default collaboration settings | ||||
|     created_at = Column(DateTime(timezone=True), server_default=func.now()) | ||||
|     updated_at = Column(DateTime(timezone=True), onupdate=func.now()) | ||||
|      | ||||
|     def to_dict(self): | ||||
|         return { | ||||
|             "id": self.id, | ||||
|             "name": self.name, | ||||
|             "display_name": self.display_name, | ||||
|             "system_prompt": self.system_prompt, | ||||
|             "reports_to": self.reports_to, | ||||
|             "expertise": self.expertise, | ||||
|             "deliverables": self.deliverables, | ||||
|             "capabilities": self.capabilities, | ||||
|             "collaboration_defaults": self.collaboration_defaults, | ||||
|             "created_at": self.created_at.isoformat() if self.created_at else None, | ||||
|             "updated_at": self.updated_at.isoformat() if self.updated_at else None | ||||
|         } | ||||
|  | ||||
|  | ||||
| class AgentCollaboration(Base): | ||||
|     __tablename__ = "agent_collaborations" | ||||
|  | ||||
|     id = Column(String, primary_key=True, index=True) | ||||
|     from_agent_id = Column(String, nullable=False, index=True)  # References agents(id) | ||||
|     to_agent_id = Column(String, nullable=True, index=True)  # References agents(id), can be null for broadcasts | ||||
|     message_type = Column(String, nullable=False)  # Type of collaboration message | ||||
|     thread_id = Column(String, nullable=True, index=True)  # Conversation thread ID | ||||
|     project_id = Column(String, nullable=True, index=True)  # Associated project | ||||
|     message_data = Column(JSON, nullable=True)  # Original message data | ||||
|     response_data = Column(JSON, nullable=True)  # Response data | ||||
|     status = Column(String, default="pending", index=True)  # pending, responded, escalated, resolved | ||||
|     priority = Column(String, default="medium", index=True)  # low, medium, high, urgent | ||||
|     created_at = Column(DateTime(timezone=True), server_default=func.now()) | ||||
|     responded_at = Column(DateTime(timezone=True), nullable=True) | ||||
|     resolved_at = Column(DateTime(timezone=True), nullable=True) | ||||
|      | ||||
|     def to_dict(self): | ||||
|         return { | ||||
|             "id": self.id, | ||||
|             "from_agent_id": self.from_agent_id, | ||||
|             "to_agent_id": self.to_agent_id, | ||||
|             "message_type": self.message_type, | ||||
|             "thread_id": self.thread_id, | ||||
|             "project_id": self.project_id, | ||||
|             "message_data": self.message_data, | ||||
|             "response_data": self.response_data, | ||||
|             "status": self.status, | ||||
|             "priority": self.priority, | ||||
|             "created_at": self.created_at.isoformat() if self.created_at else None, | ||||
|             "responded_at": self.responded_at.isoformat() if self.responded_at else None, | ||||
|             "resolved_at": self.resolved_at.isoformat() if self.resolved_at else None | ||||
|         } | ||||
| @@ -23,8 +23,7 @@ class Project(Base): | ||||
|     private_repo = Column(Boolean, default=False) | ||||
|     github_token_required = Column(Boolean, default=False) | ||||
|      | ||||
|     # Additional metadata | ||||
|     metadata = Column(JSON, nullable=True) | ||||
|     # Additional configuration | ||||
|     tags = Column(JSON, nullable=True) | ||||
|      | ||||
|     created_at = Column(DateTime(timezone=True), server_default=func.now()) | ||||
|   | ||||
							
								
								
									
										477
									
								
								backend/app/services/repository_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										477
									
								
								backend/app/services/repository_service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,477 @@ | ||||
| """ | ||||
| Repository service for managing task monitoring across different providers (GitHub, Gitea) | ||||
| """ | ||||
|  | ||||
| import asyncio | ||||
| import json | ||||
| import logging | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Dict, List, Optional, Any, Tuple | ||||
| from sqlalchemy.orm import Session | ||||
| from sqlalchemy import and_, or_ | ||||
|  | ||||
| from ..core.database import get_db | ||||
| from ..models.project import Project | ||||
| from ..models.agent import Agent | ||||
| from .agent_service import AgentService | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| class RepositoryService: | ||||
|     def __init__(self): | ||||
|         self.agent_service = AgentService() | ||||
|         self._task_cache = {} | ||||
|         self._last_sync = {} | ||||
|      | ||||
|     async def sync_all_repositories(self, db: Session) -> Dict[str, Any]: | ||||
|         """Sync tasks from all enabled repositories""" | ||||
|         results = { | ||||
|             "synced_projects": 0, | ||||
|             "new_tasks": 0, | ||||
|             "assigned_tasks": 0, | ||||
|             "errors": [] | ||||
|         } | ||||
|          | ||||
|         # Get all active projects with bzzz enabled | ||||
|         projects = db.query(Project).filter( | ||||
|             and_( | ||||
|                 Project.status == "active", | ||||
|                 Project.bzzz_enabled == True | ||||
|             ) | ||||
|         ).all() | ||||
|          | ||||
|         for project in projects: | ||||
|             try: | ||||
|                 sync_result = await self.sync_project_tasks(db, project) | ||||
|                 results["synced_projects"] += 1 | ||||
|                 results["new_tasks"] += sync_result.get("new_tasks", 0) | ||||
|                 results["assigned_tasks"] += sync_result.get("assigned_tasks", 0) | ||||
|                  | ||||
|             except Exception as e: | ||||
|                 error_msg = f"Failed to sync project {project.name}: {str(e)}" | ||||
|                 logger.error(error_msg) | ||||
|                 results["errors"].append(error_msg) | ||||
|          | ||||
|         return results | ||||
|      | ||||
|     async def sync_project_tasks(self, db: Session, project: Project) -> Dict[str, Any]: | ||||
|         """Sync tasks for a specific project""" | ||||
|         result = { | ||||
|             "project_id": project.id, | ||||
|             "project_name": project.name, | ||||
|             "new_tasks": 0, | ||||
|             "assigned_tasks": 0, | ||||
|             "provider": project.provider or "github" | ||||
|         } | ||||
|          | ||||
|         try: | ||||
|             # Get repository client based on provider | ||||
|             repo_client = await self._get_repository_client(project) | ||||
|             if not repo_client: | ||||
|                 raise Exception(f"Could not create repository client for {project.provider}") | ||||
|              | ||||
|             # Fetch available tasks | ||||
|             tasks = await repo_client.list_available_tasks() | ||||
|             result["new_tasks"] = len(tasks) | ||||
|              | ||||
|             # Process each task for potential assignment | ||||
|             for task in tasks: | ||||
|                 try: | ||||
|                     assigned = await self._process_task_for_assignment(db, project, task) | ||||
|                     if assigned: | ||||
|                         result["assigned_tasks"] += 1 | ||||
|                 except Exception as e: | ||||
|                     logger.error(f"Failed to process task {task.get('number', 'unknown')}: {str(e)}") | ||||
|              | ||||
|             # Update last sync time | ||||
|             self._last_sync[project.id] = datetime.now() | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error syncing project {project.name}: {str(e)}") | ||||
|             raise | ||||
|          | ||||
|         return result | ||||
|      | ||||
|     async def _get_repository_client(self, project: Project): | ||||
|         """Get appropriate repository client based on project provider""" | ||||
|         provider = project.provider or "github" | ||||
|          | ||||
|         if provider == "gitea": | ||||
|             return await self._create_gitea_client(project) | ||||
|         elif provider == "github": | ||||
|             return await self._create_github_client(project) | ||||
|         else: | ||||
|             raise ValueError(f"Unsupported provider: {provider}") | ||||
|      | ||||
|     async def _create_gitea_client(self, project: Project): | ||||
|         """Create Gitea API client""" | ||||
|         try: | ||||
|             import aiohttp | ||||
|              | ||||
|             class GiteaClient: | ||||
|                 def __init__(self, base_url: str, owner: str, repo: str, token: str = None): | ||||
|                     self.base_url = base_url.rstrip('/') | ||||
|                     self.owner = owner | ||||
|                     self.repo = repo | ||||
|                     self.token = token | ||||
|                     self.session = None | ||||
|                  | ||||
|                 async def list_available_tasks(self) -> List[Dict]: | ||||
|                     """List open issues with bzzz-task label""" | ||||
|                     if not self.session: | ||||
|                         self.session = aiohttp.ClientSession() | ||||
|                      | ||||
|                     url = f"{self.base_url}/api/v1/repos/{self.owner}/{self.repo}/issues" | ||||
|                     params = { | ||||
|                         "state": "open", | ||||
|                         "labels": "bzzz-task", | ||||
|                         "limit": 50 | ||||
|                     } | ||||
|                      | ||||
|                     headers = {} | ||||
|                     if self.token: | ||||
|                         headers["Authorization"] = f"token {self.token}" | ||||
|                      | ||||
|                     async with self.session.get(url, params=params, headers=headers) as response: | ||||
|                         if response.status == 200: | ||||
|                             issues = await response.json() | ||||
|                             return [self._convert_issue_to_task(issue) for issue in issues  | ||||
|                                    if not issue.get("assignee")]  # Only unassigned tasks | ||||
|                         else: | ||||
|                             logger.error(f"Gitea API error: {response.status}") | ||||
|                             return [] | ||||
|                  | ||||
|                 def _convert_issue_to_task(self, issue: Dict) -> Dict: | ||||
|                     """Convert Gitea issue to task format""" | ||||
|                     labels = [label["name"] for label in issue.get("labels", [])] | ||||
|                      | ||||
|                     # Extract role and expertise from labels | ||||
|                     required_role = self._extract_required_role(labels) | ||||
|                     required_expertise = self._extract_required_expertise(labels) | ||||
|                     priority = self._extract_priority(labels) | ||||
|                      | ||||
|                     return { | ||||
|                         "id": issue["id"], | ||||
|                         "number": issue["number"], | ||||
|                         "title": issue["title"], | ||||
|                         "description": issue.get("body", ""), | ||||
|                         "state": issue["state"], | ||||
|                         "labels": labels, | ||||
|                         "created_at": issue["created_at"], | ||||
|                         "updated_at": issue["updated_at"], | ||||
|                         "provider": "gitea", | ||||
|                         "repository": f"{self.owner}/{self.repo}", | ||||
|                         "required_role": required_role, | ||||
|                         "required_expertise": required_expertise, | ||||
|                         "priority": priority, | ||||
|                         "task_type": self._extract_task_type(labels, issue.get("body", "")), | ||||
|                         "url": issue.get("html_url", "") | ||||
|                     } | ||||
|                  | ||||
|                 def _extract_required_role(self, labels: List[str]) -> str: | ||||
|                     """Extract required role from labels""" | ||||
|                     role_map = { | ||||
|                         "frontend": "frontend_developer", | ||||
|                         "backend": "backend_developer",  | ||||
|                         "security": "security_expert", | ||||
|                         "design": "ui_ux_designer", | ||||
|                         "devops": "devops_engineer", | ||||
|                         "documentation": "technical_writer", | ||||
|                         "bug": "qa_engineer", | ||||
|                         "architecture": "senior_software_architect" | ||||
|                     } | ||||
|                      | ||||
|                     for label in labels: | ||||
|                         label_lower = label.lower() | ||||
|                         if label_lower in role_map: | ||||
|                             return role_map[label_lower] | ||||
|                      | ||||
|                     return "full_stack_engineer"  # Default | ||||
|                  | ||||
|                 def _extract_required_expertise(self, labels: List[str]) -> List[str]: | ||||
|                     """Extract required expertise from labels""" | ||||
|                     expertise = [] | ||||
|                     expertise_map = { | ||||
|                         "frontend": ["frontend", "javascript", "ui_development"], | ||||
|                         "backend": ["backend", "api_development", "server_frameworks"], | ||||
|                         "database": ["database", "sql", "data_modeling"], | ||||
|                         "security": ["security", "cybersecurity", "vulnerability_analysis"], | ||||
|                         "testing": ["testing", "qa_methodologies", "debugging"], | ||||
|                         "devops": ["deployment", "infrastructure", "automation"], | ||||
|                         "design": ["design", "user_experience", "prototyping"] | ||||
|                     } | ||||
|                      | ||||
|                     for label in labels: | ||||
|                         label_lower = label.lower() | ||||
|                         if label_lower in expertise_map: | ||||
|                             expertise.extend(expertise_map[label_lower]) | ||||
|                      | ||||
|                     return list(set(expertise)) if expertise else ["general_development"] | ||||
|                  | ||||
|                 def _extract_priority(self, labels: List[str]) -> int: | ||||
|                     """Extract priority from labels""" | ||||
|                     for label in labels: | ||||
|                         if "priority-" in label.lower(): | ||||
|                             try: | ||||
|                                 return int(label.lower().split("priority-")[1]) | ||||
|                             except (ValueError, IndexError): | ||||
|                                 pass | ||||
|                         elif label.lower() in ["urgent", "critical"]: | ||||
|                             return 10 | ||||
|                         elif label.lower() in ["high"]: | ||||
|                             return 8 | ||||
|                         elif label.lower() in ["low"]: | ||||
|                             return 3 | ||||
|                      | ||||
|                     return 5  # Default priority | ||||
|                  | ||||
|                 def _extract_task_type(self, labels: List[str], body: str) -> str: | ||||
|                     """Extract task type from labels and body""" | ||||
|                     for label in labels: | ||||
|                         label_lower = label.lower() | ||||
|                         if label_lower in ["bug", "bugfix"]: | ||||
|                             return "bug_fix" | ||||
|                         elif label_lower in ["enhancement", "feature"]: | ||||
|                             return "feature" | ||||
|                         elif label_lower in ["documentation", "docs"]: | ||||
|                             return "documentation" | ||||
|                         elif label_lower in ["security"]: | ||||
|                             return "security" | ||||
|                         elif label_lower in ["refactor", "refactoring"]: | ||||
|                             return "refactoring" | ||||
|                      | ||||
|                     return "general" | ||||
|                  | ||||
|                 async def close(self): | ||||
|                     if self.session: | ||||
|                         await self.session.close() | ||||
|              | ||||
|             # Create and return Gitea client | ||||
|             base_url = project.provider_base_url or "http://192.168.1.113:3000" | ||||
|             token = None  # TODO: Get from secure storage | ||||
|              | ||||
|             return GiteaClient( | ||||
|                 base_url=base_url, | ||||
|                 owner=project.git_owner, | ||||
|                 repo=project.git_repository, | ||||
|                 token=token | ||||
|             ) | ||||
|              | ||||
|         except ImportError: | ||||
|             logger.error("aiohttp not available for Gitea client") | ||||
|             return None | ||||
|         except Exception as e: | ||||
|             logger.error(f"Failed to create Gitea client: {str(e)}") | ||||
|             return None | ||||
|      | ||||
|     async def _create_github_client(self, project: Project): | ||||
|         """Create GitHub API client (placeholder for now)""" | ||||
|         # TODO: Implement GitHub client similar to Gitea | ||||
|         logger.warning("GitHub client not yet implemented") | ||||
|         return None | ||||
|      | ||||
|     async def _process_task_for_assignment(self, db: Session, project: Project, task: Dict) -> bool: | ||||
|         """Process a task for automatic assignment to suitable agents""" | ||||
|         try: | ||||
|             # Check if auto-assignment is enabled for this project | ||||
|             if not getattr(project, 'auto_assignment', True): | ||||
|                 return False | ||||
|              | ||||
|             # Check if task was already processed recently | ||||
|             task_key = f"{project.id}:{task['number']}" | ||||
|             if task_key in self._task_cache: | ||||
|                 return False | ||||
|              | ||||
|             # Find suitable agents for this task | ||||
|             suitable_agents = await self._find_suitable_agents(db, task) | ||||
|              | ||||
|             if not suitable_agents: | ||||
|                 logger.info(f"No suitable agents found for task {task['number']} in {project.name}") | ||||
|                 return False | ||||
|              | ||||
|             # Select best agent (first in sorted list) | ||||
|             selected_agent = suitable_agents[0] | ||||
|              | ||||
|             # Log the assignment attempt | ||||
|             await self._log_task_assignment(db, project, task, selected_agent, "auto_assigned") | ||||
|              | ||||
|             # Cache this task to avoid reprocessing | ||||
|             self._task_cache[task_key] = { | ||||
|                 "assigned_at": datetime.now(), | ||||
|                 "agent_id": selected_agent["id"], | ||||
|                 "task": task | ||||
|             } | ||||
|              | ||||
|             logger.info(f"Assigned task {task['number']} to agent {selected_agent['id']} ({selected_agent['role']})") | ||||
|             return True | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error processing task {task.get('number', 'unknown')} for assignment: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     async def _find_suitable_agents(self, db: Session, task: Dict) -> List[Dict]: | ||||
|         """Find agents suitable for a task based on role and expertise""" | ||||
|         try: | ||||
|             # Get all online agents | ||||
|             agents = db.query(Agent).filter( | ||||
|                 and_( | ||||
|                     Agent.status.in_(["online", "ready"]), | ||||
|                     Agent.role.isnot(None)  # Only agents with assigned roles | ||||
|                 ) | ||||
|             ).all() | ||||
|              | ||||
|             if not agents: | ||||
|                 return [] | ||||
|              | ||||
|             # Convert to dict format for scoring | ||||
|             agent_infos = [] | ||||
|             for agent in agents: | ||||
|                 agent_info = { | ||||
|                     "id": agent.id, | ||||
|                     "role": agent.role, | ||||
|                     "expertise": agent.expertise or [], | ||||
|                     "current_tasks": agent.current_tasks or 0, | ||||
|                     "max_tasks": agent.max_concurrent or 2, | ||||
|                     "performance": 0.8,  # Default performance score | ||||
|                     "availability": 1.0 if agent.status == "ready" else 0.7, | ||||
|                     "last_seen": agent.last_seen or datetime.now() | ||||
|                 } | ||||
|                 agent_infos.append(agent_info) | ||||
|              | ||||
|             # Score agents for this task | ||||
|             scored_agents = [] | ||||
|             for agent_info in agent_infos: | ||||
|                 # Skip if agent is at capacity | ||||
|                 if agent_info["current_tasks"] >= agent_info["max_tasks"]: | ||||
|                     continue | ||||
|                  | ||||
|                 score = self._calculate_agent_task_score(task, agent_info) | ||||
|                 if score > 0.3:  # Minimum threshold | ||||
|                     scored_agents.append({ | ||||
|                         **agent_info, | ||||
|                         "score": score | ||||
|                     }) | ||||
|              | ||||
|             # Sort by score (highest first) | ||||
|             scored_agents.sort(key=lambda x: x["score"], reverse=True) | ||||
|              | ||||
|             return scored_agents[:3]  # Return top 3 candidates | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error finding suitable agents: {str(e)}") | ||||
|             return [] | ||||
|      | ||||
|     def _calculate_agent_task_score(self, task: Dict, agent_info: Dict) -> float: | ||||
|         """Calculate how suitable an agent is for a task""" | ||||
|         score = 0.0 | ||||
|          | ||||
|         # Role matching | ||||
|         task_role = task.get("required_role", "") | ||||
|         agent_role = agent_info.get("role", "") | ||||
|          | ||||
|         if task_role == agent_role: | ||||
|             score += 0.5  # Perfect role match | ||||
|         elif self._is_compatible_role(task_role, agent_role): | ||||
|             score += 0.3  # Compatible role | ||||
|         elif agent_role == "full_stack_engineer": | ||||
|             score += 0.2  # Full-stack can handle most tasks | ||||
|          | ||||
|         # Expertise matching | ||||
|         task_expertise = task.get("required_expertise", []) | ||||
|         agent_expertise = agent_info.get("expertise", []) | ||||
|          | ||||
|         if task_expertise and agent_expertise: | ||||
|             expertise_overlap = len(set(task_expertise) & set(agent_expertise)) | ||||
|             expertise_score = expertise_overlap / len(task_expertise) | ||||
|             score += expertise_score * 0.3 | ||||
|          | ||||
|         # Priority bonus | ||||
|         priority = task.get("priority", 5) | ||||
|         priority_bonus = (priority / 10.0) * 0.1 | ||||
|         score += priority_bonus | ||||
|          | ||||
|         # Availability bonus | ||||
|         availability = agent_info.get("availability", 1.0) | ||||
|         score *= availability | ||||
|          | ||||
|         # Workload penalty | ||||
|         current_tasks = agent_info.get("current_tasks", 0) | ||||
|         max_tasks = agent_info.get("max_tasks", 2) | ||||
|         workload_ratio = current_tasks / max_tasks | ||||
|         workload_penalty = workload_ratio * 0.2 | ||||
|         score -= workload_penalty | ||||
|          | ||||
|         return max(0.0, min(1.0, score)) | ||||
|      | ||||
|     def _is_compatible_role(self, required_role: str, agent_role: str) -> bool: | ||||
|         """Check if agent role is compatible with required role""" | ||||
|         compatibility_map = { | ||||
|             "frontend_developer": ["full_stack_engineer", "ui_ux_designer"], | ||||
|             "backend_developer": ["full_stack_engineer", "database_engineer"], | ||||
|             "qa_engineer": ["full_stack_engineer"], | ||||
|             "devops_engineer": ["systems_engineer", "backend_developer"], | ||||
|             "security_expert": ["backend_developer", "senior_software_architect"], | ||||
|             "ui_ux_designer": ["frontend_developer"], | ||||
|             "technical_writer": ["full_stack_engineer"], | ||||
|             "database_engineer": ["backend_developer", "full_stack_engineer"], | ||||
|         } | ||||
|          | ||||
|         compatible_roles = compatibility_map.get(required_role, []) | ||||
|         return agent_role in compatible_roles | ||||
|      | ||||
|     async def _log_task_assignment(self, db: Session, project: Project, task: Dict, agent: Dict, reason: str): | ||||
|         """Log task assignment for tracking""" | ||||
|         try: | ||||
|             # This would insert into task_assignments table | ||||
|             # For now, just log it | ||||
|             logger.info(f"Task assignment: Project={project.name}, Task={task['number']}, " | ||||
|                        f"Agent={agent['id']}, Role={agent['role']}, Reason={reason}") | ||||
|         except Exception as e: | ||||
|             logger.error(f"Failed to log task assignment: {str(e)}") | ||||
|      | ||||
|     async def get_project_task_stats(self, db: Session, project_id: int) -> Dict[str, Any]: | ||||
|         """Get task statistics for a project""" | ||||
|         try: | ||||
|             project = db.query(Project).filter(Project.id == project_id).first() | ||||
|             if not project: | ||||
|                 return {"error": "Project not found"} | ||||
|              | ||||
|             # Get recent sync info | ||||
|             last_sync = self._last_sync.get(project_id) | ||||
|              | ||||
|             # Count cached tasks for this project | ||||
|             project_tasks = [ | ||||
|                 task_info for task_key, task_info in self._task_cache.items() | ||||
|                 if task_key.startswith(f"{project_id}:") | ||||
|             ] | ||||
|              | ||||
|             return { | ||||
|                 "project_id": project_id, | ||||
|                 "project_name": project.name, | ||||
|                 "provider": project.provider or "github", | ||||
|                 "last_sync": last_sync.isoformat() if last_sync else None, | ||||
|                 "cached_tasks": len(project_tasks), | ||||
|                 "bzzz_enabled": project.bzzz_enabled, | ||||
|                 "auto_assignment": getattr(project, "auto_assignment", True) | ||||
|             } | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error getting project task stats: {str(e)}") | ||||
|             return {"error": str(e)} | ||||
|      | ||||
|     async def cleanup_old_cache(self, max_age_hours: int = 24): | ||||
|         """Clean up old task cache entries""" | ||||
|         cutoff_time = datetime.now() - timedelta(hours=max_age_hours) | ||||
|          | ||||
|         to_remove = [] | ||||
|         for task_key, task_info in self._task_cache.items(): | ||||
|             if task_info["assigned_at"] < cutoff_time: | ||||
|                 to_remove.append(task_key) | ||||
|          | ||||
|         for key in to_remove: | ||||
|             del self._task_cache[key] | ||||
|          | ||||
|         logger.info(f"Cleaned up {len(to_remove)} old task cache entries") | ||||
|  | ||||
| # Global instance | ||||
| repository_service = RepositoryService() | ||||
							
								
								
									
										171
									
								
								backend/migrations/004_add_agent_roles.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								backend/migrations/004_add_agent_roles.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| -- Migration to add role-based collaboration fields to agents table | ||||
|  | ||||
| -- Add role-based fields to agents table | ||||
| ALTER TABLE agents ADD COLUMN role VARCHAR(255); | ||||
| ALTER TABLE agents ADD COLUMN system_prompt TEXT; | ||||
| ALTER TABLE agents ADD COLUMN reports_to JSONB; -- Array of roles this agent reports to | ||||
| ALTER TABLE agents ADD COLUMN expertise JSONB; -- Array of expertise areas | ||||
| ALTER TABLE agents ADD COLUMN deliverables JSONB; -- Array of deliverables this agent produces | ||||
| ALTER TABLE agents ADD COLUMN collaboration_settings JSONB; -- Collaboration preferences | ||||
|  | ||||
| -- Add indexes for role-based queries | ||||
| CREATE INDEX idx_agents_role ON agents(role); | ||||
| CREATE INDEX idx_agents_expertise ON agents USING GIN(expertise); | ||||
| CREATE INDEX idx_agents_reports_to ON agents USING GIN(reports_to); | ||||
|  | ||||
| -- Create agent_roles table for predefined role definitions | ||||
| CREATE TABLE agent_roles ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     name VARCHAR(255) UNIQUE NOT NULL, | ||||
|     display_name VARCHAR(255) NOT NULL, | ||||
|     system_prompt TEXT NOT NULL, | ||||
|     reports_to JSONB, -- Array of roles this role reports to | ||||
|     expertise JSONB, -- Array of expertise areas | ||||
|     deliverables JSONB, -- Array of deliverables | ||||
|     capabilities JSONB, -- Array of capabilities | ||||
|     collaboration_defaults JSONB, -- Default collaboration settings | ||||
|     created_at TIMESTAMP DEFAULT NOW(), | ||||
|     updated_at TIMESTAMP DEFAULT NOW() | ||||
| ); | ||||
|  | ||||
| -- Create index for agent_roles | ||||
| CREATE INDEX idx_agent_roles_name ON agent_roles(name); | ||||
| CREATE INDEX idx_agent_roles_expertise ON agent_roles USING GIN(expertise); | ||||
|  | ||||
| -- Insert predefined roles from Bees-AgenticWorkers.md | ||||
| INSERT INTO agent_roles (name, display_name, system_prompt, reports_to, expertise, deliverables, capabilities, collaboration_defaults) VALUES | ||||
| ( | ||||
|     'senior_software_architect', | ||||
|     'Senior Software Architect', | ||||
|     'You are the **Senior Software Architect**. You define the system''s overall structure, select tech stacks, and ensure long-term maintainability. | ||||
|  | ||||
| * **Responsibilities:** Draft high-level architecture diagrams, define API contracts, set coding standards, mentor engineering leads. | ||||
| * **Expertise:** Deep experience in multiple programming paradigms, distributed systems, security models, and cloud architectures. | ||||
| * **Reports To:** Product Owner / Technical Director. | ||||
| * **Deliverables:** Architecture blueprints, tech stack decisions, integration strategies, and review sign-offs on major design changes.', | ||||
|     '["product_owner", "technical_director"]'::jsonb, | ||||
|     '["architecture", "distributed_systems", "security", "cloud_architectures", "api_design"]'::jsonb, | ||||
|     '["architecture_blueprints", "tech_stack_decisions", "integration_strategies", "design_reviews"]'::jsonb, | ||||
|     '["task-coordination", "meta-discussion", "architecture", "code-review", "mentoring"]'::jsonb, | ||||
|     '{ | ||||
|         "preferred_message_types": ["coordination_request", "meta_discussion", "escalation_trigger"], | ||||
|         "auto_subscribe_to_roles": ["lead_designer", "security_expert", "systems_engineer"], | ||||
|         "auto_subscribe_to_expertise": ["architecture", "security", "infrastructure"], | ||||
|         "response_timeout_seconds": 300, | ||||
|         "max_collaboration_depth": 5, | ||||
|         "escalation_threshold": 3 | ||||
|     }'::jsonb | ||||
| ), | ||||
| ( | ||||
|     'lead_designer', | ||||
|     'Lead Designer', | ||||
|     'You are the **Lead Designer**. You guide the creative vision and maintain design cohesion across the product. | ||||
|  | ||||
| * **Responsibilities:** Oversee UX flow, wireframes, and feature design; ensure consistency of theme and style; mediate between product vision and technical constraints. | ||||
| * **Expertise:** UI/UX principles, accessibility, information architecture, Figma/Sketch proficiency. | ||||
| * **Reports To:** Product Owner. | ||||
| * **Deliverables:** Style guides, wireframes, feature specs, and iterative design documentation.', | ||||
|     '["product_owner"]'::jsonb, | ||||
|     '["ui_ux", "accessibility", "information_architecture", "design_systems", "user_research"]'::jsonb, | ||||
|     '["style_guides", "wireframes", "feature_specs", "design_documentation"]'::jsonb, | ||||
|     '["task-coordination", "meta-discussion", "design", "user_experience"]'::jsonb, | ||||
|     '{ | ||||
|         "preferred_message_types": ["task_help_request", "coordination_request", "meta_discussion"], | ||||
|         "auto_subscribe_to_roles": ["ui_ux_designer", "frontend_developer"], | ||||
|         "auto_subscribe_to_expertise": ["design", "frontend", "user_experience"], | ||||
|         "response_timeout_seconds": 180, | ||||
|         "max_collaboration_depth": 3, | ||||
|         "escalation_threshold": 2 | ||||
|     }'::jsonb | ||||
| ), | ||||
| ( | ||||
|     'security_expert', | ||||
|     'Security Expert', | ||||
|     'You are the **Security Expert**. You ensure the system is hardened against vulnerabilities. | ||||
|  | ||||
| * **Responsibilities:** Conduct threat modeling, penetration tests, code reviews for security flaws, and define access control policies. | ||||
| * **Expertise:** Cybersecurity frameworks (OWASP, NIST), encryption, key management, zero-trust systems. | ||||
| * **Reports To:** Senior Software Architect. | ||||
| * **Deliverables:** Security audits, vulnerability reports, risk mitigation plans, compliance documentation.', | ||||
|     '["senior_software_architect"]'::jsonb, | ||||
|     '["cybersecurity", "owasp", "nist", "encryption", "key_management", "zero_trust", "penetration_testing"]'::jsonb, | ||||
|     '["security_audits", "vulnerability_reports", "risk_mitigation_plans", "compliance_documentation"]'::jsonb, | ||||
|     '["task-coordination", "meta-discussion", "security-analysis", "code-review", "threat-modeling"]'::jsonb, | ||||
|     '{ | ||||
|         "preferred_message_types": ["dependency_alert", "task_help_request", "escalation_trigger"], | ||||
|         "auto_subscribe_to_roles": ["backend_developer", "devops_engineer", "senior_software_architect"], | ||||
|         "auto_subscribe_to_expertise": ["security", "backend", "infrastructure"], | ||||
|         "response_timeout_seconds": 120, | ||||
|         "max_collaboration_depth": 4, | ||||
|         "escalation_threshold": 1 | ||||
|     }'::jsonb | ||||
| ), | ||||
| ( | ||||
|     'frontend_developer', | ||||
|     'Frontend Developer', | ||||
|     'You are the **Frontend Developer**. You turn designs into interactive interfaces. | ||||
|  | ||||
| * **Responsibilities:** Build UI components, optimize performance, ensure cross-browser/device compatibility, and integrate frontend with backend APIs. | ||||
| * **Expertise:** HTML, CSS, JavaScript/TypeScript, React/Vue/Angular, accessibility standards. | ||||
| * **Reports To:** Frontend Lead or Senior Architect. | ||||
| * **Deliverables:** Functional UI screens, reusable components, and documented frontend code.', | ||||
|     '["frontend_lead", "senior_software_architect"]'::jsonb, | ||||
|     '["html", "css", "javascript", "typescript", "react", "vue", "angular", "accessibility"]'::jsonb, | ||||
|     '["ui_screens", "reusable_components", "frontend_code", "documentation"]'::jsonb, | ||||
|     '["task-coordination", "meta-discussion", "frontend", "ui_development", "component_design"]'::jsonb, | ||||
|     '{ | ||||
|         "preferred_message_types": ["task_help_request", "coordination_request", "task_help_response"], | ||||
|         "auto_subscribe_to_roles": ["ui_ux_designer", "backend_developer", "lead_designer"], | ||||
|         "auto_subscribe_to_expertise": ["design", "backend", "api_integration"], | ||||
|         "response_timeout_seconds": 180, | ||||
|         "max_collaboration_depth": 3, | ||||
|         "escalation_threshold": 2 | ||||
|     }'::jsonb | ||||
| ), | ||||
| ( | ||||
|     'backend_developer', | ||||
|     'Backend Developer', | ||||
|     'You are the **Backend Developer**. You create APIs, logic, and server-side integrations. | ||||
|  | ||||
| * **Responsibilities:** Implement core logic, manage data pipelines, enforce security, and support scaling strategies. | ||||
| * **Expertise:** Server frameworks, REST/GraphQL APIs, authentication, caching, microservices. | ||||
| * **Reports To:** Backend Lead or Senior Architect. | ||||
| * **Deliverables:** API endpoints, backend services, unit tests, and deployment-ready server code.', | ||||
|     '["backend_lead", "senior_software_architect"]'::jsonb, | ||||
|     '["server_frameworks", "rest_api", "graphql", "authentication", "caching", "microservices", "databases"]'::jsonb, | ||||
|     '["api_endpoints", "backend_services", "unit_tests", "server_code"]'::jsonb, | ||||
|     '["task-coordination", "meta-discussion", "backend", "api_development", "database_design"]'::jsonb, | ||||
|     '{ | ||||
|         "preferred_message_types": ["task_help_request", "coordination_request", "dependency_alert"], | ||||
|         "auto_subscribe_to_roles": ["database_engineer", "frontend_developer", "security_expert"], | ||||
|         "auto_subscribe_to_expertise": ["database", "frontend", "security"], | ||||
|         "response_timeout_seconds": 200, | ||||
|         "max_collaboration_depth": 4, | ||||
|         "escalation_threshold": 2 | ||||
|     }'::jsonb | ||||
| ); | ||||
|  | ||||
| -- Create agent_collaborations table to track collaboration history | ||||
| CREATE TABLE agent_collaborations ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     from_agent_id UUID REFERENCES agents(id), | ||||
|     to_agent_id UUID REFERENCES agents(id), | ||||
|     message_type VARCHAR(100) NOT NULL, | ||||
|     thread_id UUID, | ||||
|     project_id INTEGER REFERENCES projects(id), | ||||
|     message_data JSONB, | ||||
|     response_data JSONB, | ||||
|     status VARCHAR(50) DEFAULT 'pending', -- pending, responded, escalated, resolved | ||||
|     priority VARCHAR(20) DEFAULT 'medium', -- low, medium, high, urgent | ||||
|     created_at TIMESTAMP DEFAULT NOW(), | ||||
|     responded_at TIMESTAMP, | ||||
|     resolved_at TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Indexes for collaboration tracking | ||||
| CREATE INDEX idx_agent_collaborations_from_agent ON agent_collaborations(from_agent_id); | ||||
| CREATE INDEX idx_agent_collaborations_to_agent ON agent_collaborations(to_agent_id); | ||||
| CREATE INDEX idx_agent_collaborations_thread ON agent_collaborations(thread_id); | ||||
| CREATE INDEX idx_agent_collaborations_project ON agent_collaborations(project_id); | ||||
| CREATE INDEX idx_agent_collaborations_status ON agent_collaborations(status); | ||||
| CREATE INDEX idx_agent_collaborations_priority ON agent_collaborations(priority); | ||||
							
								
								
									
										142
									
								
								backend/migrations/005_add_gitea_repositories.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								backend/migrations/005_add_gitea_repositories.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| -- Migration to add Gitea repositories and update existing projects | ||||
|  | ||||
| -- Add provider field to projects table to distinguish between GitHub and Gitea | ||||
| ALTER TABLE projects ADD COLUMN provider VARCHAR(50) DEFAULT 'github'; | ||||
| ALTER TABLE projects ADD COLUMN provider_base_url VARCHAR(255); | ||||
| ALTER TABLE projects ADD COLUMN ssh_port INTEGER DEFAULT 22; | ||||
|  | ||||
| -- Add Gitea-specific configuration | ||||
| ALTER TABLE projects ADD COLUMN gitea_enabled BOOLEAN DEFAULT false; | ||||
| ALTER TABLE projects ADD COLUMN webhook_secret VARCHAR(255); | ||||
| ALTER TABLE projects ADD COLUMN auto_assignment BOOLEAN DEFAULT true; | ||||
|  | ||||
| -- Update existing projects to mark them as GitHub | ||||
| UPDATE projects SET provider = 'github', provider_base_url = 'https://github.com' WHERE provider IS NULL; | ||||
|  | ||||
| -- Add Gitea repositories | ||||
| INSERT INTO projects ( | ||||
|     name,  | ||||
|     description,  | ||||
|     status,  | ||||
|     github_repo,  | ||||
|     git_url,  | ||||
|     git_owner,  | ||||
|     git_repository,  | ||||
|     git_branch,  | ||||
|     bzzz_enabled,  | ||||
|     ready_to_claim,  | ||||
|     private_repo,  | ||||
|     github_token_required, | ||||
|     provider, | ||||
|     provider_base_url, | ||||
|     ssh_port, | ||||
|     gitea_enabled, | ||||
|     auto_assignment | ||||
| ) VALUES  | ||||
| ( | ||||
|     'hive-gitea',  | ||||
|     'Distributed task coordination system with AI agents (Gitea)',  | ||||
|     'active',  | ||||
|     'tony/hive',  | ||||
|     'ssh://git@192.168.1.113:2222/tony/hive.git',  | ||||
|     'tony',  | ||||
|     'hive',  | ||||
|     'master',  | ||||
|     true,  | ||||
|     true,  | ||||
|     false,  | ||||
|     false, | ||||
|     'gitea', | ||||
|     'http://192.168.1.113:3000', | ||||
|     2222, | ||||
|     true, | ||||
|     true | ||||
| ), | ||||
| ( | ||||
|     'bzzz-gitea',  | ||||
|     'P2P collaborative development coordination system (Gitea)',  | ||||
|     'active',  | ||||
|     'tony/bzzz',  | ||||
|     'ssh://git@192.168.1.113:2222/tony/bzzz.git',  | ||||
|     'tony',  | ||||
|     'bzzz',  | ||||
|     'main',  | ||||
|     true,  | ||||
|     true,  | ||||
|     false,  | ||||
|     false, | ||||
|     'gitea', | ||||
|     'http://192.168.1.113:3000', | ||||
|     2222, | ||||
|     true, | ||||
|     true | ||||
| ); | ||||
|  | ||||
| -- Create repository_config table for provider-specific configuration | ||||
| CREATE TABLE repository_config ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, | ||||
|     provider VARCHAR(50) NOT NULL, | ||||
|     config_data JSONB NOT NULL, | ||||
|     created_at TIMESTAMP DEFAULT NOW(), | ||||
|     updated_at TIMESTAMP DEFAULT NOW() | ||||
| ); | ||||
|  | ||||
| -- Insert default Gitea configuration for our repositories | ||||
| INSERT INTO repository_config (project_id, provider, config_data)  | ||||
| SELECT  | ||||
|     p.id, | ||||
|     'gitea', | ||||
|     jsonb_build_object( | ||||
|         'base_url', p.provider_base_url, | ||||
|         'owner', p.git_owner, | ||||
|         'repository', p.git_repository, | ||||
|         'task_label', 'bzzz-task', | ||||
|         'in_progress_label', 'in-progress', | ||||
|         'completed_label', 'completed', | ||||
|         'base_branch', p.git_branch, | ||||
|         'branch_prefix', 'bzzz/task-', | ||||
|         'auto_assignment', p.auto_assignment, | ||||
|         'ssh_port', p.ssh_port | ||||
|     ) | ||||
| FROM projects p  | ||||
| WHERE p.provider = 'gitea'; | ||||
|  | ||||
| -- Create task assignment log table | ||||
| CREATE TABLE task_assignments ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     project_id INTEGER REFERENCES projects(id), | ||||
|     task_number INTEGER NOT NULL, | ||||
|     agent_id VARCHAR(255) NOT NULL, | ||||
|     agent_role VARCHAR(255), | ||||
|     assignment_reason TEXT, | ||||
|     status VARCHAR(50) DEFAULT 'assigned', -- assigned, in_progress, completed, failed | ||||
|     assigned_at TIMESTAMP DEFAULT NOW(), | ||||
|     started_at TIMESTAMP, | ||||
|     completed_at TIMESTAMP, | ||||
|     results JSONB, | ||||
|     error_message TEXT | ||||
| ); | ||||
|  | ||||
| -- Create indexes for task assignments | ||||
| CREATE INDEX idx_task_assignments_project ON task_assignments(project_id); | ||||
| CREATE INDEX idx_task_assignments_agent ON task_assignments(agent_id); | ||||
| CREATE INDEX idx_task_assignments_status ON task_assignments(status); | ||||
| CREATE INDEX idx_task_assignments_task ON task_assignments(project_id, task_number); | ||||
|  | ||||
| -- Create webhook events table for tracking repository events | ||||
| CREATE TABLE webhook_events ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     project_id INTEGER REFERENCES projects(id), | ||||
|     event_type VARCHAR(100) NOT NULL, | ||||
|     payload JSONB NOT NULL, | ||||
|     processed BOOLEAN DEFAULT false, | ||||
|     processed_at TIMESTAMP, | ||||
|     created_at TIMESTAMP DEFAULT NOW() | ||||
| ); | ||||
|  | ||||
| -- Create indexes for webhook events | ||||
| CREATE INDEX idx_webhook_events_project ON webhook_events(project_id); | ||||
| CREATE INDEX idx_webhook_events_type ON webhook_events(event_type); | ||||
| CREATE INDEX idx_webhook_events_processed ON webhook_events(processed); | ||||
| CREATE INDEX idx_webhook_events_created ON webhook_events(created_at); | ||||
							
								
								
									
										30
									
								
								backend/migrations/006_add_gitea_support.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								backend/migrations/006_add_gitea_support.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| -- Migration 006: Add Gitea and Multi-Provider Support | ||||
| -- This migration adds fields for supporting multiple Git providers like Gitea | ||||
|  | ||||
| -- Add new columns to projects table | ||||
| ALTER TABLE projects  | ||||
| ADD COLUMN IF NOT EXISTS provider VARCHAR(50) DEFAULT 'github'; | ||||
|  | ||||
| ALTER TABLE projects  | ||||
| ADD COLUMN IF NOT EXISTS provider_base_url VARCHAR(255) NULL; | ||||
|  | ||||
| ALTER TABLE projects  | ||||
| ADD COLUMN IF NOT EXISTS auto_assignment BOOLEAN DEFAULT true; | ||||
|  | ||||
| -- Rename metadata column to avoid SQLAlchemy conflict | ||||
| ALTER TABLE projects  | ||||
| RENAME COLUMN metadata TO project_metadata; | ||||
|  | ||||
| -- Update existing records to have default provider | ||||
| UPDATE projects SET provider = 'github' WHERE provider IS NULL; | ||||
|  | ||||
| -- Create index for provider for better queries | ||||
| CREATE INDEX IF NOT EXISTS idx_projects_provider ON projects(provider); | ||||
| CREATE INDEX IF NOT EXISTS idx_projects_bzzz_enabled ON projects(bzzz_enabled); | ||||
| CREATE INDEX IF NOT EXISTS idx_projects_auto_assignment ON projects(auto_assignment); | ||||
|  | ||||
| -- Add comments for documentation | ||||
| COMMENT ON COLUMN projects.provider IS 'Git provider type: github, gitea, gitlab, etc.'; | ||||
| COMMENT ON COLUMN projects.provider_base_url IS 'Base URL for self-hosted providers like Gitea'; | ||||
| COMMENT ON COLUMN projects.auto_assignment IS 'Enable automatic task assignment to agents'; | ||||
| COMMENT ON COLUMN projects.project_metadata IS 'Additional project metadata as JSON'; | ||||
| @@ -117,7 +117,7 @@ export interface APIError { | ||||
|  | ||||
| // Unified API configuration | ||||
| export const API_CONFIG = { | ||||
|   BASE_URL: process.env.VITE_API_BASE_URL || 'https://hive.home.deepblack.cloud', | ||||
|   BASE_URL: process.env.VITE_API_BASE_URL || 'https://api.hive.home.deepblack.cloud', | ||||
|   TIMEOUT: 30000, | ||||
|   RETRY_ATTEMPTS: 3, | ||||
|   RETRY_DELAY: 1000, | ||||
|   | ||||
							
								
								
									
										376
									
								
								frontend/src/components/agents/AgentManagement.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								frontend/src/components/agents/AgentManagement.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,376 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | ||||
| import { Button } from '../ui/button'; | ||||
| import { Badge } from '../ui/badge'; | ||||
| import { DataTable } from '../ui/DataTable'; | ||||
| import { Alert, AlertDescription } from '../ui/alert'; | ||||
| import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'; | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; | ||||
| import AgentRoleSelector from './AgentRoleSelector'; | ||||
| import CollaborationDashboard from './CollaborationDashboard'; | ||||
|  | ||||
| interface Agent { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   endpoint: string; | ||||
|   model: string; | ||||
|   status: string; | ||||
|   role?: string; | ||||
|   expertise?: string[]; | ||||
|   reports_to?: string[]; | ||||
|   deliverables?: string[]; | ||||
|   capabilities?: string[]; | ||||
|   collaboration_settings?: any; | ||||
|   last_seen?: string; | ||||
| } | ||||
|  | ||||
| interface RoleDefinition { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   display_name: string; | ||||
|   system_prompt: string; | ||||
|   reports_to: string[]; | ||||
|   expertise: string[]; | ||||
|   deliverables: string[]; | ||||
|   capabilities: string[]; | ||||
|   collaboration_defaults: any; | ||||
| } | ||||
|  | ||||
| export const AgentManagement: React.FC = () => { | ||||
|   const [agents, setAgents] = useState<Agent[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null); | ||||
|   const [editDialogOpen, setEditDialogOpen] = useState(false); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchAgents(); | ||||
|   }, []); | ||||
|  | ||||
|   const fetchAgents = async () => { | ||||
|     try { | ||||
|       setLoading(true); | ||||
|       const response = await fetch('/api/agents'); | ||||
|       if (!response.ok) { | ||||
|         throw new Error('Failed to fetch agents'); | ||||
|       } | ||||
|       const data = await response.json(); | ||||
|       setAgents(data); | ||||
|     } catch (err) { | ||||
|       setError(err instanceof Error ? err.message : 'Failed to load agents'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleRoleUpdate = async (agent: Agent, role: RoleDefinition) => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/agents/${agent.id}`, { | ||||
|         method: 'PATCH', | ||||
|         headers: { | ||||
|           'Content-Type': 'application/json', | ||||
|         }, | ||||
|         body: JSON.stringify({ | ||||
|           role: role.name, | ||||
|           system_prompt: role.system_prompt, | ||||
|           reports_to: role.reports_to, | ||||
|           expertise: role.expertise, | ||||
|           deliverables: role.deliverables, | ||||
|           capabilities: role.capabilities, | ||||
|           collaboration_settings: role.collaboration_defaults | ||||
|         }), | ||||
|       }); | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         throw new Error('Failed to update agent role'); | ||||
|       } | ||||
|  | ||||
|       // Refresh agents list | ||||
|       await fetchAgents(); | ||||
|       setEditDialogOpen(false); | ||||
|       setSelectedAgent(null); | ||||
|     } catch (err) { | ||||
|       setError(err instanceof Error ? err.message : 'Failed to update agent'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const getStatusColor = (status: string) => { | ||||
|     switch (status) { | ||||
|       case 'online': return 'default'; | ||||
|       case 'busy': return 'secondary'; | ||||
|       case 'offline': return 'destructive'; | ||||
|       default: return 'outline'; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const agentColumns = [ | ||||
|     { | ||||
|       accessorKey: 'name', | ||||
|       header: 'Agent Name', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <div> | ||||
|           <div className="font-medium">{row.original.name}</div> | ||||
|           <div className="text-sm text-gray-500">{row.original.id.substring(0, 8)}...</div> | ||||
|         </div> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'role', | ||||
|       header: 'Role', | ||||
|       cell: ({ row }: any) => ( | ||||
|         row.original.role ? ( | ||||
|           <Badge variant="outline"> | ||||
|             {row.original.role.replace(/_/g, ' ')} | ||||
|           </Badge> | ||||
|         ) : ( | ||||
|           <span className="text-gray-400">No role assigned</span> | ||||
|         ) | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'status', | ||||
|       header: 'Status', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <Badge variant={getStatusColor(row.original.status)}> | ||||
|           {row.original.status} | ||||
|         </Badge> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'model', | ||||
|       header: 'Model', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <span className="font-mono text-sm">{row.original.model || 'N/A'}</span> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'expertise', | ||||
|       header: 'Expertise', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <div className="flex flex-wrap gap-1"> | ||||
|           {(row.original.expertise || []).slice(0, 3).map((exp: string) => ( | ||||
|             <Badge key={exp} variant="secondary" className="text-xs"> | ||||
|               {exp.replace(/_/g, ' ')} | ||||
|             </Badge> | ||||
|           ))} | ||||
|           {(row.original.expertise || []).length > 3 && ( | ||||
|             <Badge variant="outline" className="text-xs"> | ||||
|               +{(row.original.expertise || []).length - 3} more | ||||
|             </Badge> | ||||
|           )} | ||||
|         </div> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'last_seen', | ||||
|       header: 'Last Seen', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <div className="text-sm text-gray-500"> | ||||
|           {row.original.last_seen ?  | ||||
|             new Date(row.original.last_seen).toLocaleString() :  | ||||
|             'Never' | ||||
|           } | ||||
|         </div> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       id: 'actions', | ||||
|       header: 'Actions', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <Button | ||||
|           size="sm" | ||||
|           variant="outline" | ||||
|           onClick={() => { | ||||
|             setSelectedAgent(row.original); | ||||
|             setEditDialogOpen(true); | ||||
|           }} | ||||
|         > | ||||
|           Edit Role | ||||
|         </Button> | ||||
|       ) | ||||
|     } | ||||
|   ]; | ||||
|  | ||||
|   const getRoleDistribution = () => { | ||||
|     const distribution: Record<string, number> = {}; | ||||
|     agents.forEach(agent => { | ||||
|       const role = agent.role || 'unassigned'; | ||||
|       distribution[role] = (distribution[role] || 0) + 1; | ||||
|     }); | ||||
|     return distribution; | ||||
|   }; | ||||
|  | ||||
|   const getStatusDistribution = () => { | ||||
|     const distribution: Record<string, number> = {}; | ||||
|     agents.forEach(agent => { | ||||
|       distribution[agent.status] = (distribution[agent.status] || 0) + 1; | ||||
|     }); | ||||
|     return distribution; | ||||
|   }; | ||||
|  | ||||
|   if (loading) { | ||||
|     return ( | ||||
|       <div className="space-y-6"> | ||||
|         <div className="animate-pulse"> | ||||
|           <div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div> | ||||
|           <div className="h-64 bg-gray-200 rounded"></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (error) { | ||||
|     return ( | ||||
|       <Alert> | ||||
|         <AlertDescription>{error}</AlertDescription> | ||||
|         <Button onClick={fetchAgents} className="mt-4"> | ||||
|           Retry | ||||
|         </Button> | ||||
|       </Alert> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const roleDistribution = getRoleDistribution(); | ||||
|   const statusDistribution = getStatusDistribution(); | ||||
|  | ||||
|   return ( | ||||
|     <div className="space-y-6"> | ||||
|       <div className="flex justify-between items-center"> | ||||
|         <h1 className="text-3xl font-bold">Agent Management</h1> | ||||
|         <Button onClick={fetchAgents}>Refresh</Button> | ||||
|       </div> | ||||
|  | ||||
|       <Tabs defaultValue="agents" className="w-full"> | ||||
|         <TabsList> | ||||
|           <TabsTrigger value="agents">Agents</TabsTrigger> | ||||
|           <TabsTrigger value="collaborations">Collaborations</TabsTrigger> | ||||
|           <TabsTrigger value="analytics">Analytics</TabsTrigger> | ||||
|         </TabsList> | ||||
|  | ||||
|         <TabsContent value="agents" className="space-y-6"> | ||||
|           {/* Summary Cards */} | ||||
|           <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | ||||
|             <Card> | ||||
|               <CardContent className="p-4"> | ||||
|                 <div className="text-2xl font-bold">{agents.length}</div> | ||||
|                 <div className="text-sm text-gray-500">Total Agents</div> | ||||
|               </CardContent> | ||||
|             </Card> | ||||
|             <Card> | ||||
|               <CardContent className="p-4"> | ||||
|                 <div className="text-2xl font-bold">{statusDistribution.online || 0}</div> | ||||
|                 <div className="text-sm text-gray-500">Online</div> | ||||
|               </CardContent> | ||||
|             </Card> | ||||
|             <Card> | ||||
|               <CardContent className="p-4"> | ||||
|                 <div className="text-2xl font-bold">{agents.filter(a => a.role).length}</div> | ||||
|                 <div className="text-sm text-gray-500">Role Assigned</div> | ||||
|               </CardContent> | ||||
|             </Card> | ||||
|             <Card> | ||||
|               <CardContent className="p-4"> | ||||
|                 <div className="text-2xl font-bold">{Object.keys(roleDistribution).length}</div> | ||||
|                 <div className="text-sm text-gray-500">Unique Roles</div> | ||||
|               </CardContent> | ||||
|             </Card> | ||||
|           </div> | ||||
|  | ||||
|           {/* Agents Table */} | ||||
|           <Card> | ||||
|             <CardHeader> | ||||
|               <CardTitle>Registered Agents</CardTitle> | ||||
|             </CardHeader> | ||||
|             <CardContent> | ||||
|               <DataTable | ||||
|                 columns={agentColumns} | ||||
|                 data={agents} | ||||
|                 pagination={true} | ||||
|               /> | ||||
|             </CardContent> | ||||
|           </Card> | ||||
|         </TabsContent> | ||||
|  | ||||
|         <TabsContent value="collaborations"> | ||||
|           <CollaborationDashboard /> | ||||
|         </TabsContent> | ||||
|  | ||||
|         <TabsContent value="analytics" className="space-y-6"> | ||||
|           {/* Role Distribution */} | ||||
|           <Card> | ||||
|             <CardHeader> | ||||
|               <CardTitle>Role Distribution</CardTitle> | ||||
|             </CardHeader> | ||||
|             <CardContent> | ||||
|               <div className="space-y-4"> | ||||
|                 {Object.entries(roleDistribution).map(([role, count]) => ( | ||||
|                   <div key={role} className="flex justify-between items-center"> | ||||
|                     <span className="font-medium"> | ||||
|                       {role === 'unassigned' ? 'Unassigned' : role.replace(/_/g, ' ')} | ||||
|                     </span> | ||||
|                     <div className="flex items-center gap-2"> | ||||
|                       <div className="w-32 h-2 bg-gray-200 rounded"> | ||||
|                         <div  | ||||
|                           className="h-full bg-blue-500 rounded" | ||||
|                           style={{ width: `${(count / agents.length) * 100}%` }} | ||||
|                         /> | ||||
|                       </div> | ||||
|                       <span className="text-sm text-gray-500">{count}</span> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </CardContent> | ||||
|           </Card> | ||||
|  | ||||
|           {/* Status Distribution */} | ||||
|           <Card> | ||||
|             <CardHeader> | ||||
|               <CardTitle>Agent Status</CardTitle> | ||||
|             </CardHeader> | ||||
|             <CardContent> | ||||
|               <div className="space-y-4"> | ||||
|                 {Object.entries(statusDistribution).map(([status, count]) => ( | ||||
|                   <div key={status} className="flex justify-between items-center"> | ||||
|                     <div className="flex items-center gap-2"> | ||||
|                       <Badge variant={getStatusColor(status)}>{status}</Badge> | ||||
|                     </div> | ||||
|                     <div className="flex items-center gap-2"> | ||||
|                       <div className="w-32 h-2 bg-gray-200 rounded"> | ||||
|                         <div  | ||||
|                           className="h-full bg-green-500 rounded" | ||||
|                           style={{ width: `${(count / agents.length) * 100}%` }} | ||||
|                         /> | ||||
|                       </div> | ||||
|                       <span className="text-sm text-gray-500">{count}</span> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </CardContent> | ||||
|           </Card> | ||||
|         </TabsContent> | ||||
|       </Tabs> | ||||
|  | ||||
|       {/* Role Assignment Dialog */} | ||||
|       <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}> | ||||
|         <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"> | ||||
|           <DialogHeader> | ||||
|             <DialogTitle> | ||||
|               Assign Role to {selectedAgent?.name} | ||||
|             </DialogTitle> | ||||
|           </DialogHeader> | ||||
|           {selectedAgent && ( | ||||
|             <AgentRoleSelector | ||||
|               agentId={selectedAgent.id} | ||||
|               currentRole={selectedAgent.role} | ||||
|               onRoleChange={(role) => handleRoleUpdate(selectedAgent, role)} | ||||
|             /> | ||||
|           )} | ||||
|         </DialogContent> | ||||
|       </Dialog> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default AgentManagement; | ||||
							
								
								
									
										231
									
								
								frontend/src/components/agents/AgentRoleSelector.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								frontend/src/components/agents/AgentRoleSelector.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | ||||
| import { Button } from '../ui/button'; | ||||
| import { Select } from '../ui/select'; | ||||
| import { Badge } from '../ui/badge'; | ||||
| import { Input } from '../ui/input'; | ||||
| import { Textarea } from '../ui/textarea'; | ||||
| import { Label } from '../ui/label'; | ||||
| import { Alert, AlertDescription } from '../ui/alert'; | ||||
|  | ||||
| interface RoleDefinition { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   display_name: string; | ||||
|   system_prompt: string; | ||||
|   reports_to: string[]; | ||||
|   expertise: string[]; | ||||
|   deliverables: string[]; | ||||
|   capabilities: string[]; | ||||
|   collaboration_defaults: { | ||||
|     preferred_message_types: string[]; | ||||
|     auto_subscribe_to_roles: string[]; | ||||
|     auto_subscribe_to_expertise: string[]; | ||||
|     response_timeout_seconds: number; | ||||
|     max_collaboration_depth: number; | ||||
|     escalation_threshold: number; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| interface AgentRoleSelectorProps { | ||||
|   agentId?: string; | ||||
|   currentRole?: string; | ||||
|   onRoleChange: (role: RoleDefinition) => void; | ||||
|   onCustomPromptChange?: (prompt: string) => void; | ||||
|   className?: string; | ||||
| } | ||||
|  | ||||
| export const AgentRoleSelector: React.FC<AgentRoleSelectorProps> = ({ | ||||
|   agentId, | ||||
|   currentRole, | ||||
|   onRoleChange, | ||||
|   onCustomPromptChange, | ||||
|   className = "" | ||||
| }) => { | ||||
|   const [roles, setRoles] = useState<RoleDefinition[]>([]); | ||||
|   const [selectedRole, setSelectedRole] = useState<RoleDefinition | null>(null); | ||||
|   const [customPrompt, setCustomPrompt] = useState(''); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchAvailableRoles(); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (currentRole && roles.length > 0) { | ||||
|       const role = roles.find(r => r.name === currentRole); | ||||
|       if (role) { | ||||
|         setSelectedRole(role); | ||||
|       } | ||||
|     } | ||||
|   }, [currentRole, roles]); | ||||
|  | ||||
|   const fetchAvailableRoles = async () => { | ||||
|     try { | ||||
|       setLoading(true); | ||||
|       const response = await fetch('/api/agent-roles'); | ||||
|       if (!response.ok) { | ||||
|         throw new Error('Failed to fetch agent roles'); | ||||
|       } | ||||
|       const data = await response.json(); | ||||
|       setRoles(data); | ||||
|     } catch (err) { | ||||
|       setError(err instanceof Error ? err.message : 'Failed to load roles'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleRoleSelect = (roleName: string) => { | ||||
|     const role = roles.find(r => r.name === roleName); | ||||
|     if (role) { | ||||
|       setSelectedRole(role); | ||||
|       setCustomPrompt(role.system_prompt); | ||||
|       onRoleChange(role); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleCustomPromptChange = (prompt: string) => { | ||||
|     setCustomPrompt(prompt); | ||||
|     if (onCustomPromptChange) { | ||||
|       onCustomPromptChange(prompt); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (loading) { | ||||
|     return ( | ||||
|       <Card className={className}> | ||||
|         <CardHeader> | ||||
|           <CardTitle>Agent Role Configuration</CardTitle> | ||||
|         </CardHeader> | ||||
|         <CardContent> | ||||
|           <div className="animate-pulse"> | ||||
|             <div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div> | ||||
|             <div className="h-32 bg-gray-200 rounded"></div> | ||||
|           </div> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (error) { | ||||
|     return ( | ||||
|       <Card className={className}> | ||||
|         <CardHeader> | ||||
|           <CardTitle>Agent Role Configuration</CardTitle> | ||||
|         </CardHeader> | ||||
|         <CardContent> | ||||
|           <Alert> | ||||
|             <AlertDescription>{error}</AlertDescription> | ||||
|           </Alert> | ||||
|           <Button onClick={fetchAvailableRoles} className="mt-4"> | ||||
|             Retry | ||||
|           </Button> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Card className={className}> | ||||
|       <CardHeader> | ||||
|         <CardTitle>Agent Role Configuration</CardTitle> | ||||
|       </CardHeader> | ||||
|       <CardContent className="space-y-6"> | ||||
|         {/* Role Selection */} | ||||
|         <div className="space-y-2"> | ||||
|           <Label htmlFor="role-select">Select Role</Label> | ||||
|           <Select onValueChange={handleRoleSelect} value={selectedRole?.name || ''}> | ||||
|             <option value="">Select a role...</option> | ||||
|             {roles.map(role => ( | ||||
|               <option key={role.id} value={role.name}> | ||||
|                 {role.display_name} | ||||
|               </option> | ||||
|             ))} | ||||
|           </Select> | ||||
|         </div> | ||||
|  | ||||
|         {/* Role Details */} | ||||
|         {selectedRole && ( | ||||
|           <> | ||||
|             {/* Expertise Areas */} | ||||
|             <div className="space-y-2"> | ||||
|               <Label>Expertise Areas</Label> | ||||
|               <div className="flex flex-wrap gap-2"> | ||||
|                 {selectedRole.expertise.map(exp => ( | ||||
|                   <Badge key={exp} variant="secondary"> | ||||
|                     {exp.replace(/_/g, ' ')} | ||||
|                   </Badge> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
|             {/* Reports To */} | ||||
|             {selectedRole.reports_to.length > 0 && ( | ||||
|               <div className="space-y-2"> | ||||
|                 <Label>Reports To</Label> | ||||
|                 <div className="flex flex-wrap gap-2"> | ||||
|                   {selectedRole.reports_to.map(role => ( | ||||
|                     <Badge key={role} variant="outline"> | ||||
|                       {role.replace(/_/g, ' ')} | ||||
|                     </Badge> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
|  | ||||
|             {/* Deliverables */} | ||||
|             <div className="space-y-2"> | ||||
|               <Label>Key Deliverables</Label> | ||||
|               <div className="flex flex-wrap gap-2"> | ||||
|                 {selectedRole.deliverables.map(deliverable => ( | ||||
|                   <Badge key={deliverable} variant="default"> | ||||
|                     {deliverable.replace(/_/g, ' ')} | ||||
|                   </Badge> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
|             {/* Capabilities */} | ||||
|             <div className="space-y-2"> | ||||
|               <Label>Capabilities</Label> | ||||
|               <div className="flex flex-wrap gap-2"> | ||||
|                 {selectedRole.capabilities.map(capability => ( | ||||
|                   <Badge key={capability} variant="secondary"> | ||||
|                     {capability} | ||||
|                   </Badge> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
|             {/* Collaboration Settings */} | ||||
|             <div className="space-y-2"> | ||||
|               <Label>Collaboration Preferences</Label> | ||||
|               <div className="text-sm text-gray-600 space-y-1"> | ||||
|                 <div>Response Timeout: {selectedRole.collaboration_defaults.response_timeout_seconds}s</div> | ||||
|                 <div>Max Collaboration Depth: {selectedRole.collaboration_defaults.max_collaboration_depth}</div> | ||||
|                 <div>Escalation Threshold: {selectedRole.collaboration_defaults.escalation_threshold}</div> | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
|             {/* System Prompt */} | ||||
|             <div className="space-y-2"> | ||||
|               <Label htmlFor="system-prompt">System Prompt</Label> | ||||
|               <Textarea | ||||
|                 id="system-prompt" | ||||
|                 value={customPrompt} | ||||
|                 onChange={(e) => handleCustomPromptChange(e.target.value)} | ||||
|                 placeholder="System prompt for this agent role..." | ||||
|                 rows={8} | ||||
|                 className="font-mono text-sm" | ||||
|               /> | ||||
|             </div> | ||||
|           </> | ||||
|         )} | ||||
|       </CardContent> | ||||
|     </Card> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default AgentRoleSelector; | ||||
							
								
								
									
										271
									
								
								frontend/src/components/agents/CollaborationDashboard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								frontend/src/components/agents/CollaborationDashboard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,271 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | ||||
| import { Button } from '../ui/button'; | ||||
| import { Badge } from '../ui/badge'; | ||||
| import { DataTable } from '../ui/DataTable'; | ||||
| import { Alert, AlertDescription } from '../ui/alert'; | ||||
| import { Select } from '../ui/select'; | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; | ||||
|  | ||||
| interface Collaboration { | ||||
|   id: string; | ||||
|   from_agent_id: string; | ||||
|   to_agent_id: string; | ||||
|   message_type: string; | ||||
|   thread_id: string; | ||||
|   project_id: string; | ||||
|   status: string; | ||||
|   priority: string; | ||||
|   created_at: string; | ||||
|   responded_at?: string; | ||||
|   resolved_at?: string; | ||||
|   message_data: any; | ||||
|   response_data?: any; | ||||
| } | ||||
|  | ||||
| interface Agent { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   role: string; | ||||
|   status: string; | ||||
|   expertise: string[]; | ||||
| } | ||||
|  | ||||
| interface CollaborationDashboardProps { | ||||
|   className?: string; | ||||
| } | ||||
|  | ||||
| export const CollaborationDashboard: React.FC<CollaborationDashboardProps> = ({ | ||||
|   className = "" | ||||
| }) => { | ||||
|   const [collaborations, setCollaborations] = useState<Collaboration[]>([]); | ||||
|   const [agents, setAgents] = useState<Agent[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [selectedStatus, setSelectedStatus] = useState<string>('all'); | ||||
|   const [selectedMessageType, setSelectedMessageType] = useState<string>('all'); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchData(); | ||||
|   }, []); | ||||
|  | ||||
|   const fetchData = async () => { | ||||
|     try { | ||||
|       setLoading(true); | ||||
|       const [collaborationsRes, agentsRes] = await Promise.all([ | ||||
|         fetch('/api/agent-collaborations'), | ||||
|         fetch('/api/agents') | ||||
|       ]); | ||||
|  | ||||
|       if (!collaborationsRes.ok || !agentsRes.ok) { | ||||
|         throw new Error('Failed to fetch collaboration data'); | ||||
|       } | ||||
|  | ||||
|       const [collaborationsData, agentsData] = await Promise.all([ | ||||
|         collaborationsRes.json(), | ||||
|         agentsRes.json() | ||||
|       ]); | ||||
|  | ||||
|       setCollaborations(collaborationsData); | ||||
|       setAgents(agentsData); | ||||
|     } catch (err) { | ||||
|       setError(err instanceof Error ? err.message : 'Failed to load data'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const getAgentName = (agentId: string) => { | ||||
|     const agent = agents.find(a => a.id === agentId); | ||||
|     return agent ? agent.name : agentId; | ||||
|   }; | ||||
|  | ||||
|   const getAgentRole = (agentId: string) => { | ||||
|     const agent = agents.find(a => a.id === agentId); | ||||
|     return agent ? agent.role : 'Unknown'; | ||||
|   }; | ||||
|  | ||||
|   const filteredCollaborations = collaborations.filter(collab => { | ||||
|     const statusMatch = selectedStatus === 'all' || collab.status === selectedStatus; | ||||
|     const typeMatch = selectedMessageType === 'all' || collab.message_type === selectedMessageType; | ||||
|     return statusMatch && typeMatch; | ||||
|   }); | ||||
|  | ||||
|   const collaborationColumns = [ | ||||
|     { | ||||
|       accessorKey: 'from_agent_id', | ||||
|       header: 'From Agent', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <div> | ||||
|           <div className="font-medium">{getAgentName(row.original.from_agent_id)}</div> | ||||
|           <div className="text-sm text-gray-500">{getAgentRole(row.original.from_agent_id)}</div> | ||||
|         </div> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'to_agent_id', | ||||
|       header: 'To Agent', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <div> | ||||
|           <div className="font-medium">{getAgentName(row.original.to_agent_id)}</div> | ||||
|           <div className="text-sm text-gray-500">{getAgentRole(row.original.to_agent_id)}</div> | ||||
|         </div> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'message_type', | ||||
|       header: 'Message Type', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <Badge variant="outline"> | ||||
|           {row.original.message_type.replace(/_/g, ' ')} | ||||
|         </Badge> | ||||
|       ) | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'status', | ||||
|       header: 'Status', | ||||
|       cell: ({ row }: any) => { | ||||
|         const status = row.original.status; | ||||
|         const variant =  | ||||
|           status === 'resolved' ? 'default' : | ||||
|           status === 'responded' ? 'secondary' : | ||||
|           status === 'escalated' ? 'destructive' :  | ||||
|           'outline'; | ||||
|         return <Badge variant={variant}>{status}</Badge>; | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'priority', | ||||
|       header: 'Priority', | ||||
|       cell: ({ row }: any) => { | ||||
|         const priority = row.original.priority; | ||||
|         const variant =  | ||||
|           priority === 'urgent' ? 'destructive' : | ||||
|           priority === 'high' ? 'default' : | ||||
|           priority === 'medium' ? 'secondary' :  | ||||
|           'outline'; | ||||
|         return <Badge variant={variant}>{priority}</Badge>; | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessorKey: 'created_at', | ||||
|       header: 'Created', | ||||
|       cell: ({ row }: any) => ( | ||||
|         <div className="text-sm"> | ||||
|           {new Date(row.original.created_at).toLocaleDateString()} | ||||
|         </div> | ||||
|       ) | ||||
|     } | ||||
|   ]; | ||||
|  | ||||
|   const getCollaborationStats = () => { | ||||
|     const total = collaborations.length; | ||||
|     const pending = collaborations.filter(c => c.status === 'pending').length; | ||||
|     const resolved = collaborations.filter(c => c.status === 'resolved').length; | ||||
|     const escalated = collaborations.filter(c => c.status === 'escalated').length; | ||||
|  | ||||
|     return { total, pending, resolved, escalated }; | ||||
|   }; | ||||
|  | ||||
|   const stats = getCollaborationStats(); | ||||
|  | ||||
|   if (loading) { | ||||
|     return ( | ||||
|       <Card className={className}> | ||||
|         <CardHeader> | ||||
|           <CardTitle>Collaboration Dashboard</CardTitle> | ||||
|         </CardHeader> | ||||
|         <CardContent> | ||||
|           <div className="animate-pulse space-y-4"> | ||||
|             <div className="h-4 bg-gray-200 rounded w-3/4"></div> | ||||
|             <div className="h-32 bg-gray-200 rounded"></div> | ||||
|           </div> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (error) { | ||||
|     return ( | ||||
|       <Card className={className}> | ||||
|         <CardHeader> | ||||
|           <CardTitle>Collaboration Dashboard</CardTitle> | ||||
|         </CardHeader> | ||||
|         <CardContent> | ||||
|           <Alert> | ||||
|             <AlertDescription>{error}</AlertDescription> | ||||
|           </Alert> | ||||
|           <Button onClick={fetchData} className="mt-4"> | ||||
|             Retry | ||||
|           </Button> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className={`space-y-6 ${className}`}> | ||||
|       {/* Stats Cards */} | ||||
|       <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | ||||
|         <Card> | ||||
|           <CardContent className="p-4"> | ||||
|             <div className="text-2xl font-bold">{stats.total}</div> | ||||
|             <div className="text-sm text-gray-500">Total Collaborations</div> | ||||
|           </CardContent> | ||||
|         </Card> | ||||
|         <Card> | ||||
|           <CardContent className="p-4"> | ||||
|             <div className="text-2xl font-bold">{stats.pending}</div> | ||||
|             <div className="text-sm text-gray-500">Pending</div> | ||||
|           </CardContent> | ||||
|         </Card> | ||||
|         <Card> | ||||
|           <CardContent className="p-4"> | ||||
|             <div className="text-2xl font-bold">{stats.resolved}</div> | ||||
|             <div className="text-sm text-gray-500">Resolved</div> | ||||
|           </CardContent> | ||||
|         </Card> | ||||
|         <Card> | ||||
|           <CardContent className="p-4"> | ||||
|             <div className="text-2xl font-bold">{stats.escalated}</div> | ||||
|             <div className="text-sm text-gray-500">Escalated</div> | ||||
|           </CardContent> | ||||
|         </Card> | ||||
|       </div> | ||||
|  | ||||
|       {/* Main Dashboard */} | ||||
|       <Card> | ||||
|         <CardHeader> | ||||
|           <CardTitle>Agent Collaborations</CardTitle> | ||||
|           <div className="flex gap-4"> | ||||
|             <Select onValueChange={setSelectedStatus} value={selectedStatus}> | ||||
|               <option value="all">All Statuses</option> | ||||
|               <option value="pending">Pending</option> | ||||
|               <option value="responded">Responded</option> | ||||
|               <option value="resolved">Resolved</option> | ||||
|               <option value="escalated">Escalated</option> | ||||
|             </Select> | ||||
|             <Select onValueChange={setSelectedMessageType} value={selectedMessageType}> | ||||
|               <option value="all">All Message Types</option> | ||||
|               <option value="task_help_request">Help Request</option> | ||||
|               <option value="coordination_request">Coordination Request</option> | ||||
|               <option value="expertise_request">Expertise Request</option> | ||||
|               <option value="mentorship_request">Mentorship Request</option> | ||||
|               <option value="dependency_alert">Dependency Alert</option> | ||||
|               <option value="escalation_trigger">Escalation</option> | ||||
|             </Select> | ||||
|           </div> | ||||
|         </CardHeader> | ||||
|         <CardContent> | ||||
|           <DataTable | ||||
|             columns={collaborationColumns} | ||||
|             data={filteredCollaborations} | ||||
|             pagination={true} | ||||
|           /> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default CollaborationDashboard; | ||||
| @@ -4,24 +4,54 @@ | ||||
|  | ||||
| set -e | ||||
|  | ||||
| REGISTRY="anthonyrawlins" | ||||
| LOCAL_REGISTRY="registry.home.deepblack.cloud" | ||||
| REGISTRY_PORT="5000" | ||||
| NAMESPACE="tony" | ||||
| BACKEND_IMAGE="hive-backend" | ||||
| FRONTEND_IMAGE="hive-frontend" | ||||
| LOCAL_BACKEND="hive-hive-backend" | ||||
| LOCAL_FRONTEND="hive-hive-frontend" | ||||
|  | ||||
| echo "🏗️ Building and pushing Hive images to Docker Hub..." | ||||
| echo "🏗️ Building and pushing Hive images to local registry..." | ||||
|  | ||||
| # Change to hive directory | ||||
| cd "$(dirname "$0")" | ||||
|  | ||||
| # Build images with docker compose | ||||
| echo "🔨 Building images with docker compose..." | ||||
| docker compose -f docker-compose.swarm.yml build | ||||
|  | ||||
| # Get the actual image names from docker compose | ||||
| BACKEND_COMPOSE_IMAGE=$(docker compose -f docker-compose.swarm.yml config | grep "image.*hive-backend" | cut -d: -f2- | xargs) | ||||
| FRONTEND_COMPOSE_IMAGE=$(docker compose -f docker-compose.swarm.yml config | grep "image.*hive-frontend" | cut -d: -f2- | xargs) | ||||
|  | ||||
| echo "📦 Found backend image: $BACKEND_COMPOSE_IMAGE" | ||||
| echo "📦 Found frontend image: $FRONTEND_COMPOSE_IMAGE" | ||||
|  | ||||
| # Tag and push backend | ||||
| echo "📦 Pushing backend image..." | ||||
| docker tag ${LOCAL_BACKEND}:latest ${REGISTRY}/${BACKEND_IMAGE}:latest | ||||
| docker push ${REGISTRY}/${BACKEND_IMAGE}:latest | ||||
| echo "📦 Tagging and pushing backend image..." | ||||
| if [[ "$BACKEND_COMPOSE_IMAGE" != "${LOCAL_REGISTRY}/${NAMESPACE}/${BACKEND_IMAGE}:latest" ]]; then | ||||
|     # If the compose image is locally built, tag it for registry | ||||
|     LOCAL_BACKEND_IMAGE=$(docker images --format "table {{.Repository}}:{{.Tag}}" | grep hive.*backend | head -1) | ||||
|     if [[ -n "$LOCAL_BACKEND_IMAGE" ]]; then | ||||
|         docker tag "$LOCAL_BACKEND_IMAGE" "${LOCAL_REGISTRY}/${NAMESPACE}/${BACKEND_IMAGE}:latest" | ||||
|     fi | ||||
| fi | ||||
| docker push "${LOCAL_REGISTRY}/${NAMESPACE}/${BACKEND_IMAGE}:latest" | ||||
|  | ||||
| # Tag and push frontend   | ||||
| echo "📦 Pushing frontend image..." | ||||
| docker tag ${LOCAL_FRONTEND}:latest ${REGISTRY}/${FRONTEND_IMAGE}:latest | ||||
| docker push ${REGISTRY}/${FRONTEND_IMAGE}:latest | ||||
| echo "📦 Tagging and pushing frontend image..." | ||||
| if [[ "$FRONTEND_COMPOSE_IMAGE" != "${LOCAL_REGISTRY}/${NAMESPACE}/${FRONTEND_IMAGE}:latest" ]]; then | ||||
|     # If the compose image is locally built, tag it for registry | ||||
|     LOCAL_FRONTEND_IMAGE=$(docker images --format "table {{.Repository}}:{{.Tag}}" | grep hive.*frontend | head -1) | ||||
|     if [[ -n "$LOCAL_FRONTEND_IMAGE" ]]; then | ||||
|         docker tag "$LOCAL_FRONTEND_IMAGE" "${LOCAL_REGISTRY}/${NAMESPACE}/${FRONTEND_IMAGE}:latest" | ||||
|     fi | ||||
| fi | ||||
| docker push "${LOCAL_REGISTRY}/${NAMESPACE}/${FRONTEND_IMAGE}:latest" | ||||
|  | ||||
| echo "✅ Images pushed to Docker Hub successfully!" | ||||
| echo "Backend: ${REGISTRY}/${BACKEND_IMAGE}:latest" | ||||
| echo "Frontend: ${REGISTRY}/${FRONTEND_IMAGE}:latest" | ||||
| echo "✅ Images pushed to local registry successfully!" | ||||
| echo "Backend: ${LOCAL_REGISTRY}/${NAMESPACE}/${BACKEND_IMAGE}:latest" | ||||
| echo "Frontend: ${LOCAL_REGISTRY}/${NAMESPACE}/${FRONTEND_IMAGE}:latest" | ||||
| echo "" | ||||
| echo "🔍 Verify images in registry:" | ||||
| echo "curl -X GET http://localhost:${REGISTRY_PORT}/v2/${NAMESPACE}/${BACKEND_IMAGE}/tags/list" | ||||
| echo "curl -X GET http://localhost:${REGISTRY_PORT}/v2/${NAMESPACE}/${FRONTEND_IMAGE}/tags/list" | ||||
		Reference in New Issue
	
	Block a user