Files
WHOOSH/internal/composer/service.go
Claude Code 55dd5951ea
Some checks failed
WHOOSH CI / speclint (push) Has been cancelled
WHOOSH CI / contracts (push) Has been cancelled
WHOOSH CI / speclint (pull_request) Has been cancelled
WHOOSH CI / contracts (pull_request) Has been cancelled
feat: implement LLM integration for team composition engine
Resolves WHOOSH-LLM-002: Replace stubbed LLM functions with full Ollama API integration

## New Features
- Full Ollama API integration with automatic endpoint discovery
- LLM-powered task classification using configurable models
- LLM-powered skill requirement analysis
- Graceful fallback to heuristics on LLM failures
- Feature flag support for LLM vs heuristic execution
- Performance optimization with smaller, faster models (llama3.2:latest)

## Implementation Details
- Created OllamaClient with connection pooling and timeout handling
- Structured prompt engineering for consistent JSON responses
- Robust error handling with automatic failover to heuristics
- Comprehensive integration tests validating functionality
- Support for multiple Ollama endpoints with health checking

## Performance & Reliability
- Timeout configuration prevents hanging requests
- Fallback mechanism ensures system reliability
- Uses 3.2B parameter model for balance of speed vs accuracy
- Graceful degradation when LLM services unavailable

## Files Added
- internal/composer/ollama.go: Core Ollama API integration
- internal/composer/llm_test.go: Comprehensive integration tests

## Files Modified
- internal/composer/service.go: Implemented LLM functions
- internal/composer/models.go: Updated config for performance

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 21:57:16 +10:00

1019 lines
30 KiB
Go

package composer
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
)
// Service represents the Team Composer service
type Service struct {
db *pgxpool.Pool
config *ComposerConfig
ollamaClient *OllamaClient
}
// NewService creates a new Team Composer service
func NewService(db *pgxpool.Pool, config *ComposerConfig) *Service {
if config == nil {
config = DefaultComposerConfig()
}
// Initialize Ollama client for LLM operations
ollamaClient := NewOllamaClient(config.ClassificationModel)
return &Service{
db: db,
config: config,
ollamaClient: ollamaClient,
}
}
// AnalyzeAndComposeTeam performs complete task analysis and team composition
func (s *Service) AnalyzeAndComposeTeam(ctx context.Context, input *TaskAnalysisInput) (*CompositionResult, error) {
startTime := time.Now()
analysisID := uuid.New()
log.Info().
Str("analysis_id", analysisID.String()).
Str("task_title", input.Title).
Msg("Starting team composition analysis")
// Step 1: Classify the task
classification, err := s.classifyTask(ctx, input)
if err != nil {
return nil, fmt.Errorf("task classification failed: %w", err)
}
// Step 2: Analyze skill requirements
skillRequirements, err := s.analyzeSkillRequirements(ctx, input, classification)
if err != nil {
return nil, fmt.Errorf("skill analysis failed: %w", err)
}
// Step 3: Get available agents
agents, err := s.getAvailableAgents(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get available agents: %w", err)
}
// Step 4: Match agents to roles
teamComposition, err := s.composeTeam(ctx, input, classification, skillRequirements, agents)
if err != nil {
return nil, fmt.Errorf("team composition failed: %w", err)
}
processingTime := time.Since(startTime).Milliseconds()
result := &CompositionResult{
AnalysisID: analysisID,
TaskInput: input,
Classification: classification,
SkillRequirements: skillRequirements,
TeamComposition: teamComposition,
CreatedAt: time.Now(),
ProcessingTimeMs: processingTime,
}
log.Info().
Str("analysis_id", analysisID.String()).
Int64("processing_time_ms", processingTime).
Int("team_size", teamComposition.EstimatedSize).
Float64("confidence", teamComposition.ConfidenceScore).
Msg("Team composition analysis completed")
return result, nil
}
// classifyTask analyzes the task and determines its characteristics
func (s *Service) classifyTask(ctx context.Context, input *TaskAnalysisInput) (*TaskClassification, error) {
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Debug().
Str("task_title", input.Title).
Bool("llm_enabled", s.config.FeatureFlags.EnableLLMClassification).
Msg("Starting task classification")
}
// Choose classification method based on feature flag
if s.config.FeatureFlags.EnableLLMClassification {
return s.classifyTaskWithLLM(ctx, input)
}
// Use heuristic-based classification (default/reliable path)
return s.classifyTaskWithHeuristics(ctx, input)
}
// classifyTaskWithHeuristics uses rule-based classification for reliability
func (s *Service) classifyTaskWithHeuristics(ctx context.Context, input *TaskAnalysisInput) (*TaskClassification, error) {
taskType := s.determineTaskType(input.Title, input.Description)
complexity := s.estimateComplexity(input)
domains := s.identifyDomains(input.TechStack, input.Requirements)
classification := &TaskClassification{
TaskType: taskType,
ComplexityScore: complexity,
PrimaryDomains: domains[:min(len(domains), 3)], // Top 3 domains
SecondaryDomains: domains[min(len(domains), 3):], // Rest as secondary
EstimatedDuration: s.estimateDuration(complexity, len(input.Requirements)),
RiskLevel: s.assessRiskLevel(complexity, taskType),
RequiredExperience: s.determineRequiredExperience(complexity, taskType),
}
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Debug().
Str("task_type", string(taskType)).
Float64("complexity", complexity).
Strs("domains", domains).
Msg("Task classified with heuristics")
}
return classification, nil
}
// classifyTaskWithLLM uses LLM-based classification for advanced analysis
func (s *Service) classifyTaskWithLLM(ctx context.Context, input *TaskAnalysisInput) (*TaskClassification, error) {
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Info().
Str("model", s.config.ClassificationModel).
Msg("Using LLM for task classification")
}
// Create classification prompt
prompt := s.ollamaClient.BuildTaskClassificationPrompt(input)
// Set timeout for LLM operation
llmCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.AnalysisTimeoutSecs)*time.Second)
defer cancel()
// Call Ollama API
response, err := s.ollamaClient.Generate(llmCtx, prompt)
if err != nil {
if s.config.FeatureFlags.EnableFailsafeFallback {
log.Warn().
Err(err).
Msg("LLM classification failed, falling back to heuristics")
return s.classifyTaskWithHeuristics(ctx, input)
}
return nil, fmt.Errorf("LLM classification failed: %w", err)
}
// Parse LLM response
classification, err := s.ollamaClient.ParseTaskClassificationResponse(response)
if err != nil {
if s.config.FeatureFlags.EnableFailsafeFallback {
log.Warn().
Err(err).
Str("response", response).
Msg("Failed to parse LLM classification response, falling back to heuristics")
return s.classifyTaskWithHeuristics(ctx, input)
}
return nil, fmt.Errorf("failed to parse LLM classification: %w", err)
}
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Info().
Str("task_type", string(classification.TaskType)).
Float64("complexity", classification.ComplexityScore).
Strs("primary_domains", classification.PrimaryDomains).
Str("risk_level", classification.RiskLevel).
Msg("Task classified with LLM")
}
return classification, nil
}
// determineTaskType uses heuristics to classify the task type
func (s *Service) determineTaskType(title, description string) TaskType {
titleLower := strings.ToLower(title)
descLower := strings.ToLower(description)
combined := titleLower + " " + descLower
// Bug fix patterns
if strings.Contains(combined, "fix") || strings.Contains(combined, "bug") ||
strings.Contains(combined, "error") || strings.Contains(combined, "issue") {
return TaskTypeBugFix
}
// Feature development patterns
if strings.Contains(combined, "implement") || strings.Contains(combined, "add") ||
strings.Contains(combined, "create") || strings.Contains(combined, "build") {
return TaskTypeFeatureDevelopment
}
// Refactoring patterns
if strings.Contains(combined, "refactor") || strings.Contains(combined, "restructure") ||
strings.Contains(combined, "cleanup") || strings.Contains(combined, "improve") {
return TaskTypeRefactoring
}
// Security patterns
if strings.Contains(combined, "security") || strings.Contains(combined, "auth") ||
strings.Contains(combined, "encrypt") || strings.Contains(combined, "secure") {
return TaskTypeSecurity
}
// Integration patterns
if strings.Contains(combined, "integrate") || strings.Contains(combined, "connect") ||
strings.Contains(combined, "api") || strings.Contains(combined, "webhook") {
return TaskTypeIntegration
}
// Default to feature development
return TaskTypeFeatureDevelopment
}
// estimateComplexity calculates complexity score based on various factors
func (s *Service) estimateComplexity(input *TaskAnalysisInput) float64 {
complexity := 0.3 // Base complexity
// Factor in requirements count
reqCount := len(input.Requirements)
if reqCount > 10 {
complexity += 0.3
} else if reqCount > 5 {
complexity += 0.2
} else if reqCount > 2 {
complexity += 0.1
}
// Factor in tech stack diversity
techCount := len(input.TechStack)
if techCount > 5 {
complexity += 0.2
} else if techCount > 3 {
complexity += 0.1
}
// Factor in manual complexity if provided
if input.Complexity > 0 {
complexity = (complexity + input.Complexity) / 2
}
// Cap at 1.0
if complexity > 1.0 {
complexity = 1.0
}
return complexity
}
// identifyDomains extracts technical domains from tech stack and requirements
func (s *Service) identifyDomains(techStack, requirements []string) []string {
domainMap := make(map[string]bool)
// Map common technologies to domains
techDomains := map[string][]string{
"go": {"backend", "systems"},
"javascript": {"frontend", "web"},
"react": {"frontend", "web", "ui"},
"node": {"backend", "javascript"},
"python": {"backend", "data", "ml"},
"docker": {"devops", "containers"},
"postgres": {"database", "sql"},
"redis": {"cache", "database"},
"git": {"version_control"},
"api": {"backend", "integration"},
"auth": {"security", "backend"},
"test": {"testing", "quality"},
}
// Check tech stack
for _, tech := range techStack {
techLower := strings.ToLower(tech)
if domains, exists := techDomains[techLower]; exists {
for _, domain := range domains {
domainMap[domain] = true
}
} else {
// Add the tech itself as a domain if not mapped
domainMap[techLower] = true
}
}
// Check requirements for domain hints
for _, req := range requirements {
reqLower := strings.ToLower(req)
for tech, domains := range techDomains {
if strings.Contains(reqLower, tech) {
for _, domain := range domains {
domainMap[domain] = true
}
}
}
}
// Convert map to slice
domains := make([]string, 0, len(domainMap))
for domain := range domainMap {
domains = append(domains, domain)
}
return domains
}
// estimateDuration estimates hours needed based on complexity and requirements
func (s *Service) estimateDuration(complexity float64, requirementCount int) int {
baseHours := 4 // Minimum estimation
// Factor in complexity
complexityHours := int(complexity * 16) // 0.0-1.0 maps to 0-16 hours
// Factor in requirements
reqHours := requirementCount * 2 // 2 hours per requirement on average
total := baseHours + complexityHours + reqHours
// Cap reasonable limits
if total > 40 {
total = 40 // Max 1 week for MVP
}
return total
}
// assessRiskLevel determines project risk based on complexity and type
func (s *Service) assessRiskLevel(complexity float64, taskType TaskType) string {
// Base risk assessment
if complexity > 0.8 {
return "high"
} else if complexity > 0.6 {
return "medium"
} else if complexity > 0.4 {
return "low"
} else {
return "minimal"
}
}
// determineRequiredExperience maps complexity and type to experience requirements
func (s *Service) determineRequiredExperience(complexity float64, taskType TaskType) string {
// Security and integration tasks require more experience
if taskType == TaskTypeSecurity {
return "senior"
}
if complexity > 0.8 {
return "senior"
} else if complexity > 0.5 {
return "intermediate"
} else {
return "junior"
}
}
// analyzeSkillRequirements determines what skills are needed for the task
func (s *Service) analyzeSkillRequirements(ctx context.Context, input *TaskAnalysisInput, classification *TaskClassification) (*SkillRequirements, error) {
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Debug().
Str("task_title", input.Title).
Bool("llm_enabled", s.config.FeatureFlags.EnableLLMSkillAnalysis).
Msg("Starting skill requirements analysis")
}
// Choose analysis method based on feature flag
if s.config.FeatureFlags.EnableLLMSkillAnalysis {
return s.analyzeSkillRequirementsWithLLM(ctx, input, classification)
}
// Use heuristic-based analysis (default/reliable path)
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
}
// analyzeSkillRequirementsWithHeuristics uses rule-based skill analysis
func (s *Service) analyzeSkillRequirementsWithHeuristics(ctx context.Context, input *TaskAnalysisInput, classification *TaskClassification) (*SkillRequirements, error) {
critical := []SkillRequirement{}
desirable := []SkillRequirement{}
// Map domains to skill requirements
for _, domain := range classification.PrimaryDomains {
skill := SkillRequirement{
Domain: domain,
MinProficiency: 0.7, // High proficiency for primary domains
Weight: 1.0,
Critical: true,
}
critical = append(critical, skill)
}
// Secondary domains as desirable skills
for _, domain := range classification.SecondaryDomains {
skill := SkillRequirement{
Domain: domain,
MinProficiency: 0.5, // Moderate proficiency for secondary
Weight: 0.6,
Critical: false,
}
desirable = append(desirable, skill)
}
// Add task-type specific skills
switch classification.TaskType {
case TaskTypeSecurity:
critical = append(critical, SkillRequirement{
Domain: "security",
MinProficiency: 0.8,
Weight: 1.0,
Critical: true,
})
case TaskTypeBugFix:
desirable = append(desirable, SkillRequirement{
Domain: "debugging",
MinProficiency: 0.6,
Weight: 0.8,
Critical: false,
})
}
result := &SkillRequirements{
CriticalSkills: critical,
DesirableSkills: desirable,
TotalSkillCount: len(critical) + len(desirable),
}
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Debug().
Int("critical_skills", len(critical)).
Int("desirable_skills", len(desirable)).
Msg("Skills analyzed with heuristics")
}
return result, nil
}
// analyzeSkillRequirementsWithLLM uses LLM-based skill analysis
func (s *Service) analyzeSkillRequirementsWithLLM(ctx context.Context, input *TaskAnalysisInput, classification *TaskClassification) (*SkillRequirements, error) {
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Info().
Str("model", s.config.SkillAnalysisModel).
Msg("Using LLM for skill analysis")
}
// Create skill analysis prompt
prompt := s.ollamaClient.BuildSkillAnalysisPrompt(input, classification)
// Set timeout for LLM operation
llmCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.AnalysisTimeoutSecs)*time.Second)
defer cancel()
// Call Ollama API (use skill analysis model if different from classification model)
skillModel := s.config.SkillAnalysisModel
if skillModel != s.ollamaClient.model {
// Create a temporary client with the skill analysis model
skillClient := NewOllamaClient(skillModel)
response, err := skillClient.Generate(llmCtx, prompt)
if err != nil {
if s.config.FeatureFlags.EnableFailsafeFallback {
log.Warn().
Err(err).
Msg("LLM skill analysis failed, falling back to heuristics")
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
}
return nil, fmt.Errorf("LLM skill analysis failed: %w", err)
}
// Parse LLM response
skillRequirements, err := s.ollamaClient.ParseSkillRequirementsResponse(response)
if err != nil {
if s.config.FeatureFlags.EnableFailsafeFallback {
log.Warn().
Err(err).
Str("response", response).
Msg("Failed to parse LLM skill analysis response, falling back to heuristics")
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
}
return nil, fmt.Errorf("failed to parse LLM skill analysis: %w", err)
}
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Info().
Int("critical_skills", len(skillRequirements.CriticalSkills)).
Int("desirable_skills", len(skillRequirements.DesirableSkills)).
Msg("Skills analyzed with LLM")
}
return skillRequirements, nil
}
// Use the same client if models are the same
response, err := s.ollamaClient.Generate(llmCtx, prompt)
if err != nil {
if s.config.FeatureFlags.EnableFailsafeFallback {
log.Warn().
Err(err).
Msg("LLM skill analysis failed, falling back to heuristics")
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
}
return nil, fmt.Errorf("LLM skill analysis failed: %w", err)
}
// Parse LLM response
skillRequirements, err := s.ollamaClient.ParseSkillRequirementsResponse(response)
if err != nil {
if s.config.FeatureFlags.EnableFailsafeFallback {
log.Warn().
Err(err).
Str("response", response).
Msg("Failed to parse LLM skill analysis response, falling back to heuristics")
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
}
return nil, fmt.Errorf("failed to parse LLM skill analysis: %w", err)
}
if s.config.FeatureFlags.EnableAnalysisLogging {
log.Info().
Int("critical_skills", len(skillRequirements.CriticalSkills)).
Int("desirable_skills", len(skillRequirements.DesirableSkills)).
Msg("Skills analyzed with LLM")
}
return skillRequirements, nil
}
// getAvailableAgents retrieves agents that are available for assignment
func (s *Service) getAvailableAgents(ctx context.Context) ([]*Agent, error) {
query := `
SELECT id, name, endpoint_url, capabilities, status, last_seen,
performance_metrics, created_at, updated_at
FROM agents
WHERE status IN ('available', 'idle')
ORDER BY last_seen DESC
`
rows, err := s.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query agents: %w", err)
}
defer rows.Close()
var agents []*Agent
for rows.Next() {
agent := &Agent{}
var capabilitiesJSON, metricsJSON []byte
err := rows.Scan(
&agent.ID, &agent.Name, &agent.EndpointURL, &capabilitiesJSON,
&agent.Status, &agent.LastSeen, &metricsJSON,
&agent.CreatedAt, &agent.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan agent row: %w", err)
}
// Parse JSON fields
if len(capabilitiesJSON) > 0 {
json.Unmarshal(capabilitiesJSON, &agent.Capabilities)
}
if len(metricsJSON) > 0 {
json.Unmarshal(metricsJSON, &agent.PerformanceMetrics)
}
agents = append(agents, agent)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating agent rows: %w", err)
}
log.Debug().
Int("agent_count", len(agents)).
Msg("Retrieved available agents")
return agents, nil
}
// composeTeam creates the optimal team composition
func (s *Service) composeTeam(ctx context.Context, input *TaskAnalysisInput, classification *TaskClassification,
skillRequirements *SkillRequirements, agents []*Agent) (*TeamComposition, error) {
// For MVP, use simple team composition strategy
strategy := s.config.DefaultStrategy
// Get available team roles
roles, err := s.getTeamRoles(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get team roles: %w", err)
}
// Select roles based on task requirements
requiredRoles := s.selectRequiredRoles(classification, skillRequirements, roles)
// Match agents to roles
agentMatches, confidence := s.matchAgentsToRoles(agents, requiredRoles, skillRequirements)
teamID := uuid.New()
teamName := fmt.Sprintf("Team-%s", input.Title)
if len(teamName) > 50 {
teamName = teamName[:47] + "..."
}
composition := &TeamComposition{
TeamID: teamID,
Name: teamName,
Strategy: strategy,
RequiredRoles: requiredRoles,
OptionalRoles: []*TeamRole{}, // MVP: no optional roles
AgentMatches: agentMatches,
EstimatedSize: len(agentMatches),
ConfidenceScore: confidence,
}
return composition, nil
}
// getTeamRoles retrieves available team roles from database
func (s *Service) getTeamRoles(ctx context.Context) ([]*TeamRole, error) {
query := `SELECT id, name, description, capabilities, created_at FROM team_roles ORDER BY name`
rows, err := s.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query team roles: %w", err)
}
defer rows.Close()
var roles []*TeamRole
for rows.Next() {
role := &TeamRole{}
var capabilitiesJSON []byte
err := rows.Scan(&role.ID, &role.Name, &role.Description, &capabilitiesJSON, &role.CreatedAt)
if err != nil {
return nil, fmt.Errorf("failed to scan role row: %w", err)
}
if len(capabilitiesJSON) > 0 {
json.Unmarshal(capabilitiesJSON, &role.Capabilities)
}
roles = append(roles, role)
}
return roles, rows.Err()
}
// selectRequiredRoles determines which roles are needed for this task
func (s *Service) selectRequiredRoles(classification *TaskClassification, skillRequirements *SkillRequirements, availableRoles []*TeamRole) []*TeamRole {
required := []*TeamRole{}
// For MVP, simple role selection
// Always need an executor
for _, role := range availableRoles {
if role.Name == "executor" {
required = append(required, role)
break
}
}
// Add coordinator for complex tasks
if classification.ComplexityScore > 0.7 {
for _, role := range availableRoles {
if role.Name == "coordinator" {
required = append(required, role)
break
}
}
}
// Add reviewer for high-risk tasks
if classification.RiskLevel == "high" {
for _, role := range availableRoles {
if role.Name == "reviewer" {
required = append(required, role)
break
}
}
}
return required
}
// matchAgentsToRoles performs agent-to-role matching
func (s *Service) matchAgentsToRoles(agents []*Agent, roles []*TeamRole, skillRequirements *SkillRequirements) ([]*AgentMatch, float64) {
matches := []*AgentMatch{}
totalConfidence := 0.0
// For MVP, simple first-available matching
// In production, this would use sophisticated scoring algorithms
usedAgents := make(map[uuid.UUID]bool)
for _, role := range roles {
bestMatch := s.findBestAgentForRole(agents, role, skillRequirements, usedAgents)
if bestMatch != nil {
matches = append(matches, bestMatch)
usedAgents[bestMatch.Agent.ID] = true
totalConfidence += bestMatch.OverallScore
}
}
averageConfidence := totalConfidence / float64(len(matches))
return matches, averageConfidence
}
// findBestAgentForRole finds the best available agent for a specific role
func (s *Service) findBestAgentForRole(agents []*Agent, role *TeamRole, skillRequirements *SkillRequirements, usedAgents map[uuid.UUID]bool) *AgentMatch {
var bestMatch *AgentMatch
bestScore := 0.0
for _, agent := range agents {
// Skip already used agents
if usedAgents[agent.ID] {
continue
}
// Calculate match score
skillScore := s.calculateSkillMatch(agent, role, skillRequirements)
availabilityScore := s.calculateAvailabilityScore(agent)
experienceScore := s.calculateExperienceScore(agent)
overallScore := (skillScore*0.5 + availabilityScore*0.3 + experienceScore*0.2)
if overallScore > bestScore && overallScore >= s.config.SkillMatchThreshold {
bestScore = overallScore
bestMatch = &AgentMatch{
Agent: agent,
Role: role,
OverallScore: overallScore,
SkillScore: skillScore,
AvailabilityScore: availabilityScore,
ExperienceScore: experienceScore,
Reasoning: fmt.Sprintf("Matched based on skill compatibility (%.2f) and availability (%.2f)", skillScore, availabilityScore),
Confidence: overallScore,
}
}
}
return bestMatch
}
// calculateSkillMatch determines how well an agent's skills match a role
func (s *Service) calculateSkillMatch(agent *Agent, role *TeamRole, skillRequirements *SkillRequirements) float64 {
// Simple capability matching for MVP
if agent.Capabilities == nil || role.Capabilities == nil {
return 0.5 // Default moderate match
}
matchCount := 0
totalCapabilities := 0
// Check role capabilities against agent capabilities
for capability := range role.Capabilities {
totalCapabilities++
if _, hasCapability := agent.Capabilities[capability]; hasCapability {
matchCount++
}
}
if totalCapabilities == 0 {
return 0.5
}
return float64(matchCount) / float64(totalCapabilities)
}
// calculateAvailabilityScore assesses how available an agent is
func (s *Service) calculateAvailabilityScore(agent *Agent) float64 {
switch agent.Status {
case AgentStatusAvailable:
return 1.0
case AgentStatusIdle:
return 0.9
case AgentStatusBusy:
return 0.3
case AgentStatusOffline:
return 0.0
default:
return 0.5
}
}
// calculateExperienceScore evaluates agent experience from metrics
func (s *Service) calculateExperienceScore(agent *Agent) float64 {
if agent.PerformanceMetrics == nil {
return 0.5 // Default score for unknown experience
}
// Look for experience indicators in metrics
if tasksCompleted, exists := agent.PerformanceMetrics["tasks_completed"]; exists {
if count, ok := tasksCompleted.(float64); ok {
// Scale task completion count to 0-1 score
if count >= 10 {
return 1.0
} else if count >= 5 {
return 0.8
} else if count >= 1 {
return 0.6
}
}
}
return 0.5
}
// CreateTeam persists a composed team to the database
func (s *Service) CreateTeam(ctx context.Context, composition *TeamComposition, taskInput *TaskAnalysisInput) (*Team, error) {
tx, err := s.db.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
// Insert team record
team := &Team{
ID: composition.TeamID,
Name: composition.Name,
Description: fmt.Sprintf("Team for: %s", taskInput.Title),
Status: TeamStatusForming,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
insertTeamQuery := `
INSERT INTO teams (id, name, description, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err = tx.Exec(ctx, insertTeamQuery, team.ID, team.Name, team.Description, team.Status, team.CreatedAt, team.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to insert team: %w", err)
}
// Insert team assignments
for _, match := range composition.AgentMatches {
assignment := &TeamAssignment{
ID: uuid.New(),
TeamID: team.ID,
AgentID: match.Agent.ID,
RoleID: match.Role.ID,
Status: "active",
AssignedAt: time.Now(),
}
insertAssignmentQuery := `
INSERT INTO team_assignments (id, team_id, agent_id, role_id, status, assigned_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err = tx.Exec(ctx, insertAssignmentQuery,
assignment.ID, assignment.TeamID, assignment.AgentID,
assignment.RoleID, assignment.Status, assignment.AssignedAt)
if err != nil {
return nil, fmt.Errorf("failed to insert team assignment: %w", err)
}
}
if err = tx.Commit(ctx); err != nil {
return nil, fmt.Errorf("failed to commit team creation: %w", err)
}
log.Info().
Str("team_id", team.ID.String()).
Str("team_name", team.Name).
Int("members", len(composition.AgentMatches)).
Msg("Team created successfully")
return team, nil
}
// GetTeam retrieves a team with its assignments
func (s *Service) GetTeam(ctx context.Context, teamID uuid.UUID) (*Team, []*TeamAssignment, error) {
// Get team info
teamQuery := `
SELECT id, name, description, status, task_id, gitea_issue_url,
created_at, updated_at, completed_at
FROM teams WHERE id = $1
`
row := s.db.QueryRow(ctx, teamQuery, teamID)
team := &Team{}
err := row.Scan(&team.ID, &team.Name, &team.Description, &team.Status,
&team.TaskID, &team.GiteaIssueURL, &team.CreatedAt, &team.UpdatedAt, &team.CompletedAt)
if err != nil {
if err == pgx.ErrNoRows {
return nil, nil, fmt.Errorf("team not found")
}
return nil, nil, fmt.Errorf("failed to get team: %w", err)
}
// Get team assignments
assignmentQuery := `
SELECT id, team_id, agent_id, role_id, status, assigned_at, completed_at
FROM team_assignments WHERE team_id = $1 ORDER BY assigned_at
`
rows, err := s.db.Query(ctx, assignmentQuery, teamID)
if err != nil {
return nil, nil, fmt.Errorf("failed to query team assignments: %w", err)
}
defer rows.Close()
var assignments []*TeamAssignment
for rows.Next() {
assignment := &TeamAssignment{}
err := rows.Scan(&assignment.ID, &assignment.TeamID, &assignment.AgentID,
&assignment.RoleID, &assignment.Status, &assignment.AssignedAt, &assignment.CompletedAt)
if err != nil {
return nil, nil, fmt.Errorf("failed to scan assignment row: %w", err)
}
assignments = append(assignments, assignment)
}
return team, assignments, rows.Err()
}
// ListTeams retrieves all teams with pagination
func (s *Service) ListTeams(ctx context.Context, limit, offset int) ([]*Team, int, error) {
// Get total count
var total int
countRow := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM teams`)
err := countRow.Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("failed to count teams: %w", err)
}
// Get teams with pagination
teamsQuery := `
SELECT id, name, description, status, task_id, gitea_issue_url,
created_at, updated_at, completed_at
FROM teams
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
rows, err := s.db.Query(ctx, teamsQuery, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to query teams: %w", err)
}
defer rows.Close()
var teams []*Team
for rows.Next() {
team := &Team{}
err := rows.Scan(&team.ID, &team.Name, &team.Description, &team.Status,
&team.TaskID, &team.GiteaIssueURL, &team.CreatedAt, &team.UpdatedAt, &team.CompletedAt)
if err != nil {
return nil, 0, fmt.Errorf("failed to scan team row: %w", err)
}
teams = append(teams, team)
}
return teams, total, rows.Err()
}
// Public methods for testing (expose internal logic)
// DetermineTaskType exposes the internal task type determination logic
func (s *Service) DetermineTaskType(title, description string) TaskType {
return s.determineTaskType(title, description)
}
// EstimateComplexity exposes the internal complexity estimation logic
func (s *Service) EstimateComplexity(input *TaskAnalysisInput) float64 {
return s.estimateComplexity(input)
}
// IdentifyDomains exposes the internal domain identification logic
func (s *Service) IdentifyDomains(techStack, requirements []string) []string {
return s.identifyDomains(techStack, requirements)
}
// EstimateDuration exposes the internal duration estimation logic
func (s *Service) EstimateDuration(complexity float64, requirementCount int) int {
return s.estimateDuration(complexity, requirementCount)
}
// AssessRiskLevel exposes the internal risk assessment logic
func (s *Service) AssessRiskLevel(complexity float64, taskType TaskType) string {
return s.assessRiskLevel(complexity, taskType)
}
// DetermineRequiredExperience exposes the internal experience requirement logic
func (s *Service) DetermineRequiredExperience(complexity float64, taskType TaskType) string {
return s.determineRequiredExperience(complexity, taskType)
}
// AnalyzeSkillRequirementsLocal exposes skill analysis without database dependency
func (s *Service) AnalyzeSkillRequirementsLocal(input *TaskAnalysisInput, classification *TaskClassification) (*SkillRequirements, error) {
return s.analyzeSkillRequirements(context.Background(), input, classification)
}
// Helper functions
func min(a, b int) int {
if a < b {
return a
}
return b
}