2 Commits

Author SHA1 Message Date
anthonyrawlins
1e81daaf18 Fix frontend URLs for production deployment and resolve database issues
Some checks failed
Frontend Tests / unit-tests (push) Has been cancelled
Frontend Tests / e2e-tests (push) Has been cancelled
- Update API base URL from localhost to https://api.hive.home.deepblack.cloud
- Update WebSocket URL to https://hive.home.deepblack.cloud for proper TLS routing
- Remove metadata field from Project model to fix SQLAlchemy conflict
- Remove index from JSON expertise column in AgentRole to fix PostgreSQL indexing
- Update push script to use local registry instead of Docker Hub
- Add Gitea repository support and monitoring endpoints

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-28 09:16:22 +10:00
anthonyrawlins
9262e63374 Add agent role management system for Bees-AgenticWorkers integration
Backend:
- Database migration for agent role fields and predefined roles
- AgentRole and AgentCollaboration models
- Updated Agent model with role-based fields

Frontend:
- AgentRoleSelector component for role assignment
- CollaborationDashboard for monitoring agent interactions
- AgentManagement interface with role analytics

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-27 15:24:53 +10:00
14 changed files with 2125 additions and 18 deletions

View 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)}")

View File

@@ -1,4 +1,5 @@
from . import agent from . import agent
from . import agent_role
from . import project from . import project
from . import task from . import task
from . import sqlalchemy_models from . import sqlalchemy_models

View File

@@ -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.sql import func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from ..core.database import Base from ..core.database import Base
@@ -24,6 +24,14 @@ class Agent(Base):
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
last_seen = Column(DateTime(timezone=True), nullable=True) 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 # Relationships
tasks = relationship("Task", back_populates="assigned_agent") tasks = relationship("Task", back_populates="assigned_agent")
@@ -45,5 +53,13 @@ class Agent(Base):
"performance_targets": self.performance_targets, "performance_targets": self.performance_targets,
"created_at": self.created_at.isoformat() if self.created_at else None, "created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_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
} }

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

View File

@@ -23,8 +23,7 @@ class Project(Base):
private_repo = Column(Boolean, default=False) private_repo = Column(Boolean, default=False)
github_token_required = Column(Boolean, default=False) github_token_required = Column(Boolean, default=False)
# Additional metadata # Additional configuration
metadata = Column(JSON, nullable=True)
tags = Column(JSON, nullable=True) tags = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())

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

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

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

View 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';

View File

@@ -117,7 +117,7 @@ export interface APIError {
// Unified API configuration // Unified API configuration
export const API_CONFIG = { 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, TIMEOUT: 30000,
RETRY_ATTEMPTS: 3, RETRY_ATTEMPTS: 3,
RETRY_DELAY: 1000, RETRY_DELAY: 1000,

View 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;

View 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;

View 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;

View File

@@ -4,24 +4,54 @@
set -e set -e
REGISTRY="anthonyrawlins" LOCAL_REGISTRY="registry.home.deepblack.cloud"
REGISTRY_PORT="5000"
NAMESPACE="tony"
BACKEND_IMAGE="hive-backend" BACKEND_IMAGE="hive-backend"
FRONTEND_IMAGE="hive-frontend" 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 # Tag and push backend
echo "📦 Pushing backend image..." echo "📦 Tagging and pushing backend image..."
docker tag ${LOCAL_BACKEND}:latest ${REGISTRY}/${BACKEND_IMAGE}:latest if [[ "$BACKEND_COMPOSE_IMAGE" != "${LOCAL_REGISTRY}/${NAMESPACE}/${BACKEND_IMAGE}:latest" ]]; then
docker push ${REGISTRY}/${BACKEND_IMAGE}:latest # 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 # Tag and push frontend
echo "📦 Pushing frontend image..." echo "📦 Tagging and pushing frontend image..."
docker tag ${LOCAL_FRONTEND}:latest ${REGISTRY}/${FRONTEND_IMAGE}:latest if [[ "$FRONTEND_COMPOSE_IMAGE" != "${LOCAL_REGISTRY}/${NAMESPACE}/${FRONTEND_IMAGE}:latest" ]]; then
docker push ${REGISTRY}/${FRONTEND_IMAGE}:latest # 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 "✅ Images pushed to local registry successfully!"
echo "Backend: ${REGISTRY}/${BACKEND_IMAGE}:latest" echo "Backend: ${LOCAL_REGISTRY}/${NAMESPACE}/${BACKEND_IMAGE}:latest"
echo "Frontend: ${REGISTRY}/${FRONTEND_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"