Add agent role management system for Bees-AgenticWorkers integration

Backend:
- Database migration for agent role fields and predefined roles
- AgentRole and AgentCollaboration models
- Updated Agent model with role-based fields

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-27 15:24:53 +10:00
parent ef3b61740b
commit 9262e63374
7 changed files with 1137 additions and 2 deletions

View File

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

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON
from sqlalchemy import Column, Integer, String, DateTime, JSON, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from ..core.database import Base
@@ -24,6 +24,14 @@ class Agent(Base):
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
last_seen = Column(DateTime(timezone=True), nullable=True)
# Role-based collaboration fields
role = Column(String, nullable=True) # Role from Bees-AgenticWorkers
system_prompt = Column(Text, nullable=True) # Role-specific system prompt
reports_to = Column(JSON, nullable=True) # Array of roles this agent reports to
expertise = Column(JSON, nullable=True) # Array of expertise areas
deliverables = Column(JSON, nullable=True) # Array of deliverables
collaboration_settings = Column(JSON, nullable=True) # Collaboration preferences
# Relationships
tasks = relationship("Task", back_populates="assigned_agent")
@@ -45,5 +53,13 @@ class Agent(Base):
"performance_targets": self.performance_targets,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"last_seen": self.last_seen.isoformat() if self.last_seen else None
"last_seen": self.last_seen.isoformat() if self.last_seen else None,
# Role-based fields
"role": self.role,
"system_prompt": self.system_prompt,
"reports_to": self.reports_to,
"expertise": self.expertise,
"deliverables": self.deliverables,
"collaboration_settings": self.collaboration_settings
}

View File

@@ -0,0 +1,69 @@
from sqlalchemy import Column, String, DateTime, JSON, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from ..core.database import Base
class AgentRole(Base):
__tablename__ = "agent_roles"
id = Column(String, primary_key=True, index=True)
name = Column(String, unique=True, nullable=False, index=True) # Role identifier (e.g., "senior_software_architect")
display_name = Column(String, nullable=False) # Human-readable name
system_prompt = Column(Text, nullable=False) # Role-specific system prompt
reports_to = Column(JSON, nullable=True) # Array of roles this role reports to
expertise = Column(JSON, nullable=True, index=True) # Array of expertise areas
deliverables = Column(JSON, nullable=True) # Array of deliverables
capabilities = Column(JSON, nullable=True) # Array of capabilities
collaboration_defaults = Column(JSON, nullable=True) # Default collaboration settings
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"display_name": self.display_name,
"system_prompt": self.system_prompt,
"reports_to": self.reports_to,
"expertise": self.expertise,
"deliverables": self.deliverables,
"capabilities": self.capabilities,
"collaboration_defaults": self.collaboration_defaults,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None
}
class AgentCollaboration(Base):
__tablename__ = "agent_collaborations"
id = Column(String, primary_key=True, index=True)
from_agent_id = Column(String, nullable=False, index=True) # References agents(id)
to_agent_id = Column(String, nullable=True, index=True) # References agents(id), can be null for broadcasts
message_type = Column(String, nullable=False) # Type of collaboration message
thread_id = Column(String, nullable=True, index=True) # Conversation thread ID
project_id = Column(String, nullable=True, index=True) # Associated project
message_data = Column(JSON, nullable=True) # Original message data
response_data = Column(JSON, nullable=True) # Response data
status = Column(String, default="pending", index=True) # pending, responded, escalated, resolved
priority = Column(String, default="medium", index=True) # low, medium, high, urgent
created_at = Column(DateTime(timezone=True), server_default=func.now())
responded_at = Column(DateTime(timezone=True), nullable=True)
resolved_at = Column(DateTime(timezone=True), nullable=True)
def to_dict(self):
return {
"id": self.id,
"from_agent_id": self.from_agent_id,
"to_agent_id": self.to_agent_id,
"message_type": self.message_type,
"thread_id": self.thread_id,
"project_id": self.project_id,
"message_data": self.message_data,
"response_data": self.response_data,
"status": self.status,
"priority": self.priority,
"created_at": self.created_at.isoformat() if self.created_at else None,
"responded_at": self.responded_at.isoformat() if self.responded_at else None,
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None
}

View File

@@ -0,0 +1,171 @@
-- Migration to add role-based collaboration fields to agents table
-- Add role-based fields to agents table
ALTER TABLE agents ADD COLUMN role VARCHAR(255);
ALTER TABLE agents ADD COLUMN system_prompt TEXT;
ALTER TABLE agents ADD COLUMN reports_to JSONB; -- Array of roles this agent reports to
ALTER TABLE agents ADD COLUMN expertise JSONB; -- Array of expertise areas
ALTER TABLE agents ADD COLUMN deliverables JSONB; -- Array of deliverables this agent produces
ALTER TABLE agents ADD COLUMN collaboration_settings JSONB; -- Collaboration preferences
-- Add indexes for role-based queries
CREATE INDEX idx_agents_role ON agents(role);
CREATE INDEX idx_agents_expertise ON agents USING GIN(expertise);
CREATE INDEX idx_agents_reports_to ON agents USING GIN(reports_to);
-- Create agent_roles table for predefined role definitions
CREATE TABLE agent_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
system_prompt TEXT NOT NULL,
reports_to JSONB, -- Array of roles this role reports to
expertise JSONB, -- Array of expertise areas
deliverables JSONB, -- Array of deliverables
capabilities JSONB, -- Array of capabilities
collaboration_defaults JSONB, -- Default collaboration settings
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Create index for agent_roles
CREATE INDEX idx_agent_roles_name ON agent_roles(name);
CREATE INDEX idx_agent_roles_expertise ON agent_roles USING GIN(expertise);
-- Insert predefined roles from Bees-AgenticWorkers.md
INSERT INTO agent_roles (name, display_name, system_prompt, reports_to, expertise, deliverables, capabilities, collaboration_defaults) VALUES
(
'senior_software_architect',
'Senior Software Architect',
'You are the **Senior Software Architect**. You define the system''s overall structure, select tech stacks, and ensure long-term maintainability.
* **Responsibilities:** Draft high-level architecture diagrams, define API contracts, set coding standards, mentor engineering leads.
* **Expertise:** Deep experience in multiple programming paradigms, distributed systems, security models, and cloud architectures.
* **Reports To:** Product Owner / Technical Director.
* **Deliverables:** Architecture blueprints, tech stack decisions, integration strategies, and review sign-offs on major design changes.',
'["product_owner", "technical_director"]'::jsonb,
'["architecture", "distributed_systems", "security", "cloud_architectures", "api_design"]'::jsonb,
'["architecture_blueprints", "tech_stack_decisions", "integration_strategies", "design_reviews"]'::jsonb,
'["task-coordination", "meta-discussion", "architecture", "code-review", "mentoring"]'::jsonb,
'{
"preferred_message_types": ["coordination_request", "meta_discussion", "escalation_trigger"],
"auto_subscribe_to_roles": ["lead_designer", "security_expert", "systems_engineer"],
"auto_subscribe_to_expertise": ["architecture", "security", "infrastructure"],
"response_timeout_seconds": 300,
"max_collaboration_depth": 5,
"escalation_threshold": 3
}'::jsonb
),
(
'lead_designer',
'Lead Designer',
'You are the **Lead Designer**. You guide the creative vision and maintain design cohesion across the product.
* **Responsibilities:** Oversee UX flow, wireframes, and feature design; ensure consistency of theme and style; mediate between product vision and technical constraints.
* **Expertise:** UI/UX principles, accessibility, information architecture, Figma/Sketch proficiency.
* **Reports To:** Product Owner.
* **Deliverables:** Style guides, wireframes, feature specs, and iterative design documentation.',
'["product_owner"]'::jsonb,
'["ui_ux", "accessibility", "information_architecture", "design_systems", "user_research"]'::jsonb,
'["style_guides", "wireframes", "feature_specs", "design_documentation"]'::jsonb,
'["task-coordination", "meta-discussion", "design", "user_experience"]'::jsonb,
'{
"preferred_message_types": ["task_help_request", "coordination_request", "meta_discussion"],
"auto_subscribe_to_roles": ["ui_ux_designer", "frontend_developer"],
"auto_subscribe_to_expertise": ["design", "frontend", "user_experience"],
"response_timeout_seconds": 180,
"max_collaboration_depth": 3,
"escalation_threshold": 2
}'::jsonb
),
(
'security_expert',
'Security Expert',
'You are the **Security Expert**. You ensure the system is hardened against vulnerabilities.
* **Responsibilities:** Conduct threat modeling, penetration tests, code reviews for security flaws, and define access control policies.
* **Expertise:** Cybersecurity frameworks (OWASP, NIST), encryption, key management, zero-trust systems.
* **Reports To:** Senior Software Architect.
* **Deliverables:** Security audits, vulnerability reports, risk mitigation plans, compliance documentation.',
'["senior_software_architect"]'::jsonb,
'["cybersecurity", "owasp", "nist", "encryption", "key_management", "zero_trust", "penetration_testing"]'::jsonb,
'["security_audits", "vulnerability_reports", "risk_mitigation_plans", "compliance_documentation"]'::jsonb,
'["task-coordination", "meta-discussion", "security-analysis", "code-review", "threat-modeling"]'::jsonb,
'{
"preferred_message_types": ["dependency_alert", "task_help_request", "escalation_trigger"],
"auto_subscribe_to_roles": ["backend_developer", "devops_engineer", "senior_software_architect"],
"auto_subscribe_to_expertise": ["security", "backend", "infrastructure"],
"response_timeout_seconds": 120,
"max_collaboration_depth": 4,
"escalation_threshold": 1
}'::jsonb
),
(
'frontend_developer',
'Frontend Developer',
'You are the **Frontend Developer**. You turn designs into interactive interfaces.
* **Responsibilities:** Build UI components, optimize performance, ensure cross-browser/device compatibility, and integrate frontend with backend APIs.
* **Expertise:** HTML, CSS, JavaScript/TypeScript, React/Vue/Angular, accessibility standards.
* **Reports To:** Frontend Lead or Senior Architect.
* **Deliverables:** Functional UI screens, reusable components, and documented frontend code.',
'["frontend_lead", "senior_software_architect"]'::jsonb,
'["html", "css", "javascript", "typescript", "react", "vue", "angular", "accessibility"]'::jsonb,
'["ui_screens", "reusable_components", "frontend_code", "documentation"]'::jsonb,
'["task-coordination", "meta-discussion", "frontend", "ui_development", "component_design"]'::jsonb,
'{
"preferred_message_types": ["task_help_request", "coordination_request", "task_help_response"],
"auto_subscribe_to_roles": ["ui_ux_designer", "backend_developer", "lead_designer"],
"auto_subscribe_to_expertise": ["design", "backend", "api_integration"],
"response_timeout_seconds": 180,
"max_collaboration_depth": 3,
"escalation_threshold": 2
}'::jsonb
),
(
'backend_developer',
'Backend Developer',
'You are the **Backend Developer**. You create APIs, logic, and server-side integrations.
* **Responsibilities:** Implement core logic, manage data pipelines, enforce security, and support scaling strategies.
* **Expertise:** Server frameworks, REST/GraphQL APIs, authentication, caching, microservices.
* **Reports To:** Backend Lead or Senior Architect.
* **Deliverables:** API endpoints, backend services, unit tests, and deployment-ready server code.',
'["backend_lead", "senior_software_architect"]'::jsonb,
'["server_frameworks", "rest_api", "graphql", "authentication", "caching", "microservices", "databases"]'::jsonb,
'["api_endpoints", "backend_services", "unit_tests", "server_code"]'::jsonb,
'["task-coordination", "meta-discussion", "backend", "api_development", "database_design"]'::jsonb,
'{
"preferred_message_types": ["task_help_request", "coordination_request", "dependency_alert"],
"auto_subscribe_to_roles": ["database_engineer", "frontend_developer", "security_expert"],
"auto_subscribe_to_expertise": ["database", "frontend", "security"],
"response_timeout_seconds": 200,
"max_collaboration_depth": 4,
"escalation_threshold": 2
}'::jsonb
);
-- Create agent_collaborations table to track collaboration history
CREATE TABLE agent_collaborations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_agent_id UUID REFERENCES agents(id),
to_agent_id UUID REFERENCES agents(id),
message_type VARCHAR(100) NOT NULL,
thread_id UUID,
project_id INTEGER REFERENCES projects(id),
message_data JSONB,
response_data JSONB,
status VARCHAR(50) DEFAULT 'pending', -- pending, responded, escalated, resolved
priority VARCHAR(20) DEFAULT 'medium', -- low, medium, high, urgent
created_at TIMESTAMP DEFAULT NOW(),
responded_at TIMESTAMP,
resolved_at TIMESTAMP
);
-- Indexes for collaboration tracking
CREATE INDEX idx_agent_collaborations_from_agent ON agent_collaborations(from_agent_id);
CREATE INDEX idx_agent_collaborations_to_agent ON agent_collaborations(to_agent_id);
CREATE INDEX idx_agent_collaborations_thread ON agent_collaborations(thread_id);
CREATE INDEX idx_agent_collaborations_project ON agent_collaborations(project_id);
CREATE INDEX idx_agent_collaborations_status ON agent_collaborations(status);
CREATE INDEX idx_agent_collaborations_priority ON agent_collaborations(priority);

View File

@@ -0,0 +1,376 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { DataTable } from '../ui/DataTable';
import { Alert, AlertDescription } from '../ui/alert';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import AgentRoleSelector from './AgentRoleSelector';
import CollaborationDashboard from './CollaborationDashboard';
interface Agent {
id: string;
name: string;
endpoint: string;
model: string;
status: string;
role?: string;
expertise?: string[];
reports_to?: string[];
deliverables?: string[];
capabilities?: string[];
collaboration_settings?: any;
last_seen?: string;
}
interface RoleDefinition {
id: string;
name: string;
display_name: string;
system_prompt: string;
reports_to: string[];
expertise: string[];
deliverables: string[];
capabilities: string[];
collaboration_defaults: any;
}
export const AgentManagement: React.FC = () => {
const [agents, setAgents] = useState<Agent[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const [editDialogOpen, setEditDialogOpen] = useState(false);
useEffect(() => {
fetchAgents();
}, []);
const fetchAgents = async () => {
try {
setLoading(true);
const response = await fetch('/api/agents');
if (!response.ok) {
throw new Error('Failed to fetch agents');
}
const data = await response.json();
setAgents(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load agents');
} finally {
setLoading(false);
}
};
const handleRoleUpdate = async (agent: Agent, role: RoleDefinition) => {
try {
const response = await fetch(`/api/agents/${agent.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role: role.name,
system_prompt: role.system_prompt,
reports_to: role.reports_to,
expertise: role.expertise,
deliverables: role.deliverables,
capabilities: role.capabilities,
collaboration_settings: role.collaboration_defaults
}),
});
if (!response.ok) {
throw new Error('Failed to update agent role');
}
// Refresh agents list
await fetchAgents();
setEditDialogOpen(false);
setSelectedAgent(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update agent');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'online': return 'default';
case 'busy': return 'secondary';
case 'offline': return 'destructive';
default: return 'outline';
}
};
const agentColumns = [
{
accessorKey: 'name',
header: 'Agent Name',
cell: ({ row }: any) => (
<div>
<div className="font-medium">{row.original.name}</div>
<div className="text-sm text-gray-500">{row.original.id.substring(0, 8)}...</div>
</div>
)
},
{
accessorKey: 'role',
header: 'Role',
cell: ({ row }: any) => (
row.original.role ? (
<Badge variant="outline">
{row.original.role.replace(/_/g, ' ')}
</Badge>
) : (
<span className="text-gray-400">No role assigned</span>
)
)
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }: any) => (
<Badge variant={getStatusColor(row.original.status)}>
{row.original.status}
</Badge>
)
},
{
accessorKey: 'model',
header: 'Model',
cell: ({ row }: any) => (
<span className="font-mono text-sm">{row.original.model || 'N/A'}</span>
)
},
{
accessorKey: 'expertise',
header: 'Expertise',
cell: ({ row }: any) => (
<div className="flex flex-wrap gap-1">
{(row.original.expertise || []).slice(0, 3).map((exp: string) => (
<Badge key={exp} variant="secondary" className="text-xs">
{exp.replace(/_/g, ' ')}
</Badge>
))}
{(row.original.expertise || []).length > 3 && (
<Badge variant="outline" className="text-xs">
+{(row.original.expertise || []).length - 3} more
</Badge>
)}
</div>
)
},
{
accessorKey: 'last_seen',
header: 'Last Seen',
cell: ({ row }: any) => (
<div className="text-sm text-gray-500">
{row.original.last_seen ?
new Date(row.original.last_seen).toLocaleString() :
'Never'
}
</div>
)
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }: any) => (
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedAgent(row.original);
setEditDialogOpen(true);
}}
>
Edit Role
</Button>
)
}
];
const getRoleDistribution = () => {
const distribution: Record<string, number> = {};
agents.forEach(agent => {
const role = agent.role || 'unassigned';
distribution[role] = (distribution[role] || 0) + 1;
});
return distribution;
};
const getStatusDistribution = () => {
const distribution: Record<string, number> = {};
agents.forEach(agent => {
distribution[agent.status] = (distribution[agent.status] || 0) + 1;
});
return distribution;
};
if (loading) {
return (
<div className="space-y-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
</div>
);
}
if (error) {
return (
<Alert>
<AlertDescription>{error}</AlertDescription>
<Button onClick={fetchAgents} className="mt-4">
Retry
</Button>
</Alert>
);
}
const roleDistribution = getRoleDistribution();
const statusDistribution = getStatusDistribution();
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Agent Management</h1>
<Button onClick={fetchAgents}>Refresh</Button>
</div>
<Tabs defaultValue="agents" className="w-full">
<TabsList>
<TabsTrigger value="agents">Agents</TabsTrigger>
<TabsTrigger value="collaborations">Collaborations</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
</TabsList>
<TabsContent value="agents" className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{agents.length}</div>
<div className="text-sm text-gray-500">Total Agents</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{statusDistribution.online || 0}</div>
<div className="text-sm text-gray-500">Online</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{agents.filter(a => a.role).length}</div>
<div className="text-sm text-gray-500">Role Assigned</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{Object.keys(roleDistribution).length}</div>
<div className="text-sm text-gray-500">Unique Roles</div>
</CardContent>
</Card>
</div>
{/* Agents Table */}
<Card>
<CardHeader>
<CardTitle>Registered Agents</CardTitle>
</CardHeader>
<CardContent>
<DataTable
columns={agentColumns}
data={agents}
pagination={true}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="collaborations">
<CollaborationDashboard />
</TabsContent>
<TabsContent value="analytics" className="space-y-6">
{/* Role Distribution */}
<Card>
<CardHeader>
<CardTitle>Role Distribution</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{Object.entries(roleDistribution).map(([role, count]) => (
<div key={role} className="flex justify-between items-center">
<span className="font-medium">
{role === 'unassigned' ? 'Unassigned' : role.replace(/_/g, ' ')}
</span>
<div className="flex items-center gap-2">
<div className="w-32 h-2 bg-gray-200 rounded">
<div
className="h-full bg-blue-500 rounded"
style={{ width: `${(count / agents.length) * 100}%` }}
/>
</div>
<span className="text-sm text-gray-500">{count}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Status Distribution */}
<Card>
<CardHeader>
<CardTitle>Agent Status</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{Object.entries(statusDistribution).map(([status, count]) => (
<div key={status} className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Badge variant={getStatusColor(status)}>{status}</Badge>
</div>
<div className="flex items-center gap-2">
<div className="w-32 h-2 bg-gray-200 rounded">
<div
className="h-full bg-green-500 rounded"
style={{ width: `${(count / agents.length) * 100}%` }}
/>
</div>
<span className="text-sm text-gray-500">{count}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Role Assignment Dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
Assign Role to {selectedAgent?.name}
</DialogTitle>
</DialogHeader>
{selectedAgent && (
<AgentRoleSelector
agentId={selectedAgent.id}
currentRole={selectedAgent.role}
onRoleChange={(role) => handleRoleUpdate(selectedAgent, role)}
/>
)}
</DialogContent>
</Dialog>
</div>
);
};
export default AgentManagement;

View File

@@ -0,0 +1,231 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { Select } from '../ui/select';
import { Badge } from '../ui/badge';
import { Input } from '../ui/input';
import { Textarea } from '../ui/textarea';
import { Label } from '../ui/label';
import { Alert, AlertDescription } from '../ui/alert';
interface RoleDefinition {
id: string;
name: string;
display_name: string;
system_prompt: string;
reports_to: string[];
expertise: string[];
deliverables: string[];
capabilities: string[];
collaboration_defaults: {
preferred_message_types: string[];
auto_subscribe_to_roles: string[];
auto_subscribe_to_expertise: string[];
response_timeout_seconds: number;
max_collaboration_depth: number;
escalation_threshold: number;
};
}
interface AgentRoleSelectorProps {
agentId?: string;
currentRole?: string;
onRoleChange: (role: RoleDefinition) => void;
onCustomPromptChange?: (prompt: string) => void;
className?: string;
}
export const AgentRoleSelector: React.FC<AgentRoleSelectorProps> = ({
agentId,
currentRole,
onRoleChange,
onCustomPromptChange,
className = ""
}) => {
const [roles, setRoles] = useState<RoleDefinition[]>([]);
const [selectedRole, setSelectedRole] = useState<RoleDefinition | null>(null);
const [customPrompt, setCustomPrompt] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchAvailableRoles();
}, []);
useEffect(() => {
if (currentRole && roles.length > 0) {
const role = roles.find(r => r.name === currentRole);
if (role) {
setSelectedRole(role);
}
}
}, [currentRole, roles]);
const fetchAvailableRoles = async () => {
try {
setLoading(true);
const response = await fetch('/api/agent-roles');
if (!response.ok) {
throw new Error('Failed to fetch agent roles');
}
const data = await response.json();
setRoles(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load roles');
} finally {
setLoading(false);
}
};
const handleRoleSelect = (roleName: string) => {
const role = roles.find(r => r.name === roleName);
if (role) {
setSelectedRole(role);
setCustomPrompt(role.system_prompt);
onRoleChange(role);
}
};
const handleCustomPromptChange = (prompt: string) => {
setCustomPrompt(prompt);
if (onCustomPromptChange) {
onCustomPromptChange(prompt);
}
};
if (loading) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>Agent Role Configuration</CardTitle>
</CardHeader>
<CardContent>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>Agent Role Configuration</CardTitle>
</CardHeader>
<CardContent>
<Alert>
<AlertDescription>{error}</AlertDescription>
</Alert>
<Button onClick={fetchAvailableRoles} className="mt-4">
Retry
</Button>
</CardContent>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<CardTitle>Agent Role Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Role Selection */}
<div className="space-y-2">
<Label htmlFor="role-select">Select Role</Label>
<Select onValueChange={handleRoleSelect} value={selectedRole?.name || ''}>
<option value="">Select a role...</option>
{roles.map(role => (
<option key={role.id} value={role.name}>
{role.display_name}
</option>
))}
</Select>
</div>
{/* Role Details */}
{selectedRole && (
<>
{/* Expertise Areas */}
<div className="space-y-2">
<Label>Expertise Areas</Label>
<div className="flex flex-wrap gap-2">
{selectedRole.expertise.map(exp => (
<Badge key={exp} variant="secondary">
{exp.replace(/_/g, ' ')}
</Badge>
))}
</div>
</div>
{/* Reports To */}
{selectedRole.reports_to.length > 0 && (
<div className="space-y-2">
<Label>Reports To</Label>
<div className="flex flex-wrap gap-2">
{selectedRole.reports_to.map(role => (
<Badge key={role} variant="outline">
{role.replace(/_/g, ' ')}
</Badge>
))}
</div>
</div>
)}
{/* Deliverables */}
<div className="space-y-2">
<Label>Key Deliverables</Label>
<div className="flex flex-wrap gap-2">
{selectedRole.deliverables.map(deliverable => (
<Badge key={deliverable} variant="default">
{deliverable.replace(/_/g, ' ')}
</Badge>
))}
</div>
</div>
{/* Capabilities */}
<div className="space-y-2">
<Label>Capabilities</Label>
<div className="flex flex-wrap gap-2">
{selectedRole.capabilities.map(capability => (
<Badge key={capability} variant="secondary">
{capability}
</Badge>
))}
</div>
</div>
{/* Collaboration Settings */}
<div className="space-y-2">
<Label>Collaboration Preferences</Label>
<div className="text-sm text-gray-600 space-y-1">
<div>Response Timeout: {selectedRole.collaboration_defaults.response_timeout_seconds}s</div>
<div>Max Collaboration Depth: {selectedRole.collaboration_defaults.max_collaboration_depth}</div>
<div>Escalation Threshold: {selectedRole.collaboration_defaults.escalation_threshold}</div>
</div>
</div>
{/* System Prompt */}
<div className="space-y-2">
<Label htmlFor="system-prompt">System Prompt</Label>
<Textarea
id="system-prompt"
value={customPrompt}
onChange={(e) => handleCustomPromptChange(e.target.value)}
placeholder="System prompt for this agent role..."
rows={8}
className="font-mono text-sm"
/>
</div>
</>
)}
</CardContent>
</Card>
);
};
export default AgentRoleSelector;

View File

@@ -0,0 +1,271 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { DataTable } from '../ui/DataTable';
import { Alert, AlertDescription } from '../ui/alert';
import { Select } from '../ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
interface Collaboration {
id: string;
from_agent_id: string;
to_agent_id: string;
message_type: string;
thread_id: string;
project_id: string;
status: string;
priority: string;
created_at: string;
responded_at?: string;
resolved_at?: string;
message_data: any;
response_data?: any;
}
interface Agent {
id: string;
name: string;
role: string;
status: string;
expertise: string[];
}
interface CollaborationDashboardProps {
className?: string;
}
export const CollaborationDashboard: React.FC<CollaborationDashboardProps> = ({
className = ""
}) => {
const [collaborations, setCollaborations] = useState<Collaboration[]>([]);
const [agents, setAgents] = useState<Agent[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = useState<string>('all');
const [selectedMessageType, setSelectedMessageType] = useState<string>('all');
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const [collaborationsRes, agentsRes] = await Promise.all([
fetch('/api/agent-collaborations'),
fetch('/api/agents')
]);
if (!collaborationsRes.ok || !agentsRes.ok) {
throw new Error('Failed to fetch collaboration data');
}
const [collaborationsData, agentsData] = await Promise.all([
collaborationsRes.json(),
agentsRes.json()
]);
setCollaborations(collaborationsData);
setAgents(agentsData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load data');
} finally {
setLoading(false);
}
};
const getAgentName = (agentId: string) => {
const agent = agents.find(a => a.id === agentId);
return agent ? agent.name : agentId;
};
const getAgentRole = (agentId: string) => {
const agent = agents.find(a => a.id === agentId);
return agent ? agent.role : 'Unknown';
};
const filteredCollaborations = collaborations.filter(collab => {
const statusMatch = selectedStatus === 'all' || collab.status === selectedStatus;
const typeMatch = selectedMessageType === 'all' || collab.message_type === selectedMessageType;
return statusMatch && typeMatch;
});
const collaborationColumns = [
{
accessorKey: 'from_agent_id',
header: 'From Agent',
cell: ({ row }: any) => (
<div>
<div className="font-medium">{getAgentName(row.original.from_agent_id)}</div>
<div className="text-sm text-gray-500">{getAgentRole(row.original.from_agent_id)}</div>
</div>
)
},
{
accessorKey: 'to_agent_id',
header: 'To Agent',
cell: ({ row }: any) => (
<div>
<div className="font-medium">{getAgentName(row.original.to_agent_id)}</div>
<div className="text-sm text-gray-500">{getAgentRole(row.original.to_agent_id)}</div>
</div>
)
},
{
accessorKey: 'message_type',
header: 'Message Type',
cell: ({ row }: any) => (
<Badge variant="outline">
{row.original.message_type.replace(/_/g, ' ')}
</Badge>
)
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }: any) => {
const status = row.original.status;
const variant =
status === 'resolved' ? 'default' :
status === 'responded' ? 'secondary' :
status === 'escalated' ? 'destructive' :
'outline';
return <Badge variant={variant}>{status}</Badge>;
}
},
{
accessorKey: 'priority',
header: 'Priority',
cell: ({ row }: any) => {
const priority = row.original.priority;
const variant =
priority === 'urgent' ? 'destructive' :
priority === 'high' ? 'default' :
priority === 'medium' ? 'secondary' :
'outline';
return <Badge variant={variant}>{priority}</Badge>;
}
},
{
accessorKey: 'created_at',
header: 'Created',
cell: ({ row }: any) => (
<div className="text-sm">
{new Date(row.original.created_at).toLocaleDateString()}
</div>
)
}
];
const getCollaborationStats = () => {
const total = collaborations.length;
const pending = collaborations.filter(c => c.status === 'pending').length;
const resolved = collaborations.filter(c => c.status === 'resolved').length;
const escalated = collaborations.filter(c => c.status === 'escalated').length;
return { total, pending, resolved, escalated };
};
const stats = getCollaborationStats();
if (loading) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>Collaboration Dashboard</CardTitle>
</CardHeader>
<CardContent>
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>Collaboration Dashboard</CardTitle>
</CardHeader>
<CardContent>
<Alert>
<AlertDescription>{error}</AlertDescription>
</Alert>
<Button onClick={fetchData} className="mt-4">
Retry
</Button>
</CardContent>
</Card>
);
}
return (
<div className={`space-y-6 ${className}`}>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{stats.total}</div>
<div className="text-sm text-gray-500">Total Collaborations</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{stats.pending}</div>
<div className="text-sm text-gray-500">Pending</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{stats.resolved}</div>
<div className="text-sm text-gray-500">Resolved</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold">{stats.escalated}</div>
<div className="text-sm text-gray-500">Escalated</div>
</CardContent>
</Card>
</div>
{/* Main Dashboard */}
<Card>
<CardHeader>
<CardTitle>Agent Collaborations</CardTitle>
<div className="flex gap-4">
<Select onValueChange={setSelectedStatus} value={selectedStatus}>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="responded">Responded</option>
<option value="resolved">Resolved</option>
<option value="escalated">Escalated</option>
</Select>
<Select onValueChange={setSelectedMessageType} value={selectedMessageType}>
<option value="all">All Message Types</option>
<option value="task_help_request">Help Request</option>
<option value="coordination_request">Coordination Request</option>
<option value="expertise_request">Expertise Request</option>
<option value="mentorship_request">Mentorship Request</option>
<option value="dependency_alert">Dependency Alert</option>
<option value="escalation_trigger">Escalation</option>
</Select>
</div>
</CardHeader>
<CardContent>
<DataTable
columns={collaborationColumns}
data={filteredCollaborations}
pagination={true}
/>
</CardContent>
</Card>
</div>
);
};
export default CollaborationDashboard;