Compare commits
2 Commits
ef3b61740b
...
1e81daaf18
| 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
|
||||||
|
from . import agent_role
|
||||||
from . import project
|
from . import project
|
||||||
from . import task
|
from . import task
|
||||||
from . import sqlalchemy_models
|
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.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
|
||||||
}
|
}
|
||||||
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)
|
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())
|
||||||
|
|||||||
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
|
// 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,
|
||||||
|
|||||||
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
|
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"
|
||||||
Reference in New Issue
Block a user