| iomodo | dea44b0 | 2025-07-29 12:55:25 +0400 | [diff] [blame] | 1 | package task |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sort" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/iomodo/staff/config" |
| 9 | "github.com/iomodo/staff/tm" |
| 10 | ) |
| 11 | |
| 12 | // AutoAssigner handles intelligent task assignment to agents |
| 13 | type AutoAssigner struct { |
| 14 | agents []config.AgentConfig |
| 15 | } |
| 16 | |
| 17 | // NewAutoAssigner creates a new auto-assignment system |
| 18 | func NewAutoAssigner(agents []config.AgentConfig) *AutoAssigner { |
| 19 | return &AutoAssigner{ |
| 20 | agents: agents, |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | // AssignmentScore represents how well an agent matches a task |
| 25 | type AssignmentScore struct { |
| 26 | AgentName string |
| 27 | Score float64 |
| 28 | Reasons []string |
| 29 | } |
| 30 | |
| 31 | // AssignTask automatically assigns a task to the best matching agent |
| 32 | func (a *AutoAssigner) AssignTask(task *tm.Task) (string, error) { |
| 33 | if len(a.agents) == 0 { |
| 34 | return "", fmt.Errorf("no agents available for assignment") |
| 35 | } |
| 36 | |
| 37 | scores := a.calculateScores(task) |
| 38 | if len(scores) == 0 { |
| 39 | return "", fmt.Errorf("no suitable agent found for task") |
| 40 | } |
| 41 | |
| 42 | // Sort by score (highest first) |
| 43 | sort.Slice(scores, func(i, j int) bool { |
| 44 | return scores[i].Score > scores[j].Score |
| 45 | }) |
| 46 | |
| 47 | bestMatch := scores[0] |
| 48 | if bestMatch.Score == 0 { |
| 49 | // No good match, assign to CEO as fallback |
| 50 | return "ceo", nil |
| 51 | } |
| 52 | |
| 53 | return bestMatch.AgentName, nil |
| 54 | } |
| 55 | |
| 56 | // GetAssignmentRecommendations returns ranked recommendations for task assignment |
| 57 | func (a *AutoAssigner) GetAssignmentRecommendations(task *tm.Task) []AssignmentScore { |
| 58 | scores := a.calculateScores(task) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 59 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 60 | // Sort by score (highest first) |
| 61 | sort.Slice(scores, func(i, j int) bool { |
| 62 | return scores[i].Score > scores[j].Score |
| 63 | }) |
| 64 | |
| 65 | return scores |
| 66 | } |
| 67 | |
| 68 | // calculateScores calculates assignment scores for all agents |
| 69 | func (a *AutoAssigner) calculateScores(task *tm.Task) []AssignmentScore { |
| 70 | scores := make([]AssignmentScore, 0, len(a.agents)) |
| 71 | |
| 72 | taskText := strings.ToLower(task.Title + " " + task.Description) |
| 73 | taskKeywords := extractKeywords(taskText) |
| 74 | |
| 75 | for _, agent := range a.agents { |
| 76 | score := &AssignmentScore{ |
| 77 | AgentName: agent.Name, |
| 78 | Score: 0, |
| 79 | Reasons: make([]string, 0), |
| 80 | } |
| 81 | |
| 82 | // Score based on task type keywords |
| 83 | score.Score += a.scoreTaskTypes(agent.TaskTypes, taskKeywords, score) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 84 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 85 | // Score based on capabilities |
| 86 | score.Score += a.scoreCapabilities(agent.Capabilities, taskKeywords, score) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 87 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 88 | // Score based on task priority and agent model |
| 89 | score.Score += a.scorePriorityModelMatch(task.Priority, agent.Model, score) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 90 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 91 | // Score based on explicit agent mention in task |
| 92 | score.Score += a.scoreExplicitMention(agent.Name, agent.Role, taskText, score) |
| 93 | |
| 94 | scores = append(scores, *score) |
| 95 | } |
| 96 | |
| 97 | return scores |
| 98 | } |
| 99 | |
| 100 | // scoreTaskTypes scores based on task type matching |
| 101 | func (a *AutoAssigner) scoreTaskTypes(agentTypes []string, taskKeywords []string, score *AssignmentScore) float64 { |
| 102 | typeScore := 0.0 |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 103 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 104 | for _, agentType := range agentTypes { |
| 105 | for _, keyword := range taskKeywords { |
| 106 | if strings.Contains(keyword, agentType) || strings.Contains(agentType, keyword) { |
| 107 | typeScore += 3.0 |
| 108 | score.Reasons = append(score.Reasons, fmt.Sprintf("matches task type: %s", agentType)) |
| 109 | } |
| 110 | } |
| 111 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 112 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 113 | return typeScore |
| 114 | } |
| 115 | |
| 116 | // scoreCapabilities scores based on capability matching |
| 117 | func (a *AutoAssigner) scoreCapabilities(capabilities []string, taskKeywords []string, score *AssignmentScore) float64 { |
| 118 | capScore := 0.0 |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 119 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 120 | for _, capability := range capabilities { |
| 121 | for _, keyword := range taskKeywords { |
| 122 | if strings.Contains(keyword, capability) || strings.Contains(capability, keyword) { |
| 123 | capScore += 2.0 |
| 124 | score.Reasons = append(score.Reasons, fmt.Sprintf("has capability: %s", capability)) |
| 125 | } |
| 126 | } |
| 127 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 128 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 129 | return capScore |
| 130 | } |
| 131 | |
| 132 | // scorePriorityModelMatch scores based on priority and model sophistication |
| 133 | func (a *AutoAssigner) scorePriorityModelMatch(priority tm.TaskPriority, model string, score *AssignmentScore) float64 { |
| 134 | priorityScore := 0.0 |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 135 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 136 | // High priority tasks prefer more capable models |
| 137 | if priority == tm.PriorityHigh && strings.Contains(model, "gpt-4") { |
| 138 | priorityScore += 1.0 |
| 139 | score.Reasons = append(score.Reasons, "high priority task matches advanced model") |
| 140 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 141 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 142 | // Medium/low priority can use efficient models |
| 143 | if priority != tm.PriorityHigh && strings.Contains(model, "gpt-3.5") { |
| 144 | priorityScore += 0.5 |
| 145 | score.Reasons = append(score.Reasons, "priority matches model efficiency") |
| 146 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 147 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 148 | return priorityScore |
| 149 | } |
| 150 | |
| 151 | // scoreExplicitMention scores based on explicit agent/role mentions |
| 152 | func (a *AutoAssigner) scoreExplicitMention(agentName, agentRole, taskText string, score *AssignmentScore) float64 { |
| 153 | mentionScore := 0.0 |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 154 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 155 | // Check for explicit agent name mention |
| 156 | if strings.Contains(taskText, agentName) { |
| 157 | mentionScore += 5.0 |
| 158 | score.Reasons = append(score.Reasons, "explicitly mentioned by name") |
| 159 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 160 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 161 | // Check for role mention |
| 162 | roleLower := strings.ToLower(agentRole) |
| 163 | if strings.Contains(taskText, roleLower) { |
| 164 | mentionScore += 4.0 |
| 165 | score.Reasons = append(score.Reasons, "role mentioned in task") |
| 166 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 167 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 168 | return mentionScore |
| 169 | } |
| 170 | |
| 171 | // extractKeywords extracts relevant keywords from task text |
| 172 | func extractKeywords(text string) []string { |
| 173 | // Simple keyword extraction - split by common delimiters |
| 174 | words := strings.FieldsFunc(text, func(c rune) bool { |
| 175 | return c == ' ' || c == ',' || c == '.' || c == ':' || c == ';' || c == '\n' || c == '\t' |
| 176 | }) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 177 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 178 | // Filter out common stop words and short words |
| 179 | stopWords := map[string]bool{ |
| 180 | "the": true, "a": true, "an": true, "and": true, "or": true, "but": true, |
| 181 | "in": true, "on": true, "at": true, "to": true, "for": true, "of": true, |
| 182 | "with": true, "by": true, "is": true, "are": true, "was": true, "were": true, |
| 183 | "be": true, "been": true, "have": true, "has": true, "had": true, "do": true, |
| 184 | "does": true, "did": true, "will": true, "would": true, "could": true, "should": true, |
| 185 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 186 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 187 | keywords := make([]string, 0) |
| 188 | for _, word := range words { |
| 189 | word = strings.ToLower(strings.TrimSpace(word)) |
| 190 | if len(word) > 2 && !stopWords[word] { |
| 191 | keywords = append(keywords, word) |
| 192 | } |
| 193 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 194 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 195 | return keywords |
| 196 | } |
| 197 | |
| 198 | // ValidateAssignment checks if an assignment is valid |
| 199 | func (a *AutoAssigner) ValidateAssignment(agentName string, task *tm.Task) error { |
| 200 | // Check if agent exists |
| 201 | for _, agent := range a.agents { |
| 202 | if agent.Name == agentName { |
| 203 | return nil |
| 204 | } |
| 205 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 206 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 207 | return fmt.Errorf("agent '%s' not found", agentName) |
| 208 | } |
| 209 | |
| 210 | // GetAgentCapabilities returns the capabilities of a specific agent |
| 211 | func (a *AutoAssigner) GetAgentCapabilities(agentName string) ([]string, error) { |
| 212 | for _, agent := range a.agents { |
| 213 | if agent.Name == agentName { |
| 214 | return agent.Capabilities, nil |
| 215 | } |
| 216 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 217 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 218 | return nil, fmt.Errorf("agent '%s' not found", agentName) |
| 219 | } |
| 220 | |
| 221 | // GetRecommendationExplanation returns a human-readable explanation for assignment |
| 222 | func (a *AutoAssigner) GetRecommendationExplanation(task *tm.Task, agentName string) string { |
| 223 | recommendations := a.GetAssignmentRecommendations(task) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 224 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 225 | for _, rec := range recommendations { |
| 226 | if rec.AgentName == agentName { |
| 227 | if len(rec.Reasons) == 0 { |
| 228 | return fmt.Sprintf("Agent %s assigned (score: %.1f)", agentName, rec.Score) |
| 229 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 230 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 231 | reasons := strings.Join(rec.Reasons, ", ") |
| 232 | return fmt.Sprintf("Agent %s assigned (score: %.1f) because: %s", agentName, rec.Score, reasons) |
| 233 | } |
| 234 | } |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 235 | |
| user | 5a7d60d | 2025-07-27 21:22:04 +0400 | [diff] [blame] | 236 | return fmt.Sprintf("Agent %s assigned (manual override)", agentName) |
| iomodo | 50598c6 | 2025-07-27 22:06:32 +0400 | [diff] [blame] | 237 | } |