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 }