blob: 4bd56320f90eced283a1f7f9e3acdc628f776252 [file] [log] [blame]
iomododea44b02025-07-29 12:55:25 +04001package task
user5a7d60d2025-07-27 21:22:04 +04002
3import (
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
13type AutoAssigner struct {
14 agents []config.AgentConfig
15}
16
17// NewAutoAssigner creates a new auto-assignment system
18func NewAutoAssigner(agents []config.AgentConfig) *AutoAssigner {
19 return &AutoAssigner{
20 agents: agents,
21 }
22}
23
24// AssignmentScore represents how well an agent matches a task
25type AssignmentScore struct {
26 AgentName string
27 Score float64
28 Reasons []string
29}
30
31// AssignTask automatically assigns a task to the best matching agent
32func (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
57func (a *AutoAssigner) GetAssignmentRecommendations(task *tm.Task) []AssignmentScore {
58 scores := a.calculateScores(task)
iomodo50598c62025-07-27 22:06:32 +040059
user5a7d60d2025-07-27 21:22:04 +040060 // 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
69func (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)
iomodo50598c62025-07-27 22:06:32 +040084
user5a7d60d2025-07-27 21:22:04 +040085 // Score based on capabilities
86 score.Score += a.scoreCapabilities(agent.Capabilities, taskKeywords, score)
iomodo50598c62025-07-27 22:06:32 +040087
user5a7d60d2025-07-27 21:22:04 +040088 // Score based on task priority and agent model
89 score.Score += a.scorePriorityModelMatch(task.Priority, agent.Model, score)
iomodo50598c62025-07-27 22:06:32 +040090
user5a7d60d2025-07-27 21:22:04 +040091 // 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
101func (a *AutoAssigner) scoreTaskTypes(agentTypes []string, taskKeywords []string, score *AssignmentScore) float64 {
102 typeScore := 0.0
iomodo50598c62025-07-27 22:06:32 +0400103
user5a7d60d2025-07-27 21:22:04 +0400104 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 }
iomodo50598c62025-07-27 22:06:32 +0400112
user5a7d60d2025-07-27 21:22:04 +0400113 return typeScore
114}
115
116// scoreCapabilities scores based on capability matching
117func (a *AutoAssigner) scoreCapabilities(capabilities []string, taskKeywords []string, score *AssignmentScore) float64 {
118 capScore := 0.0
iomodo50598c62025-07-27 22:06:32 +0400119
user5a7d60d2025-07-27 21:22:04 +0400120 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 }
iomodo50598c62025-07-27 22:06:32 +0400128
user5a7d60d2025-07-27 21:22:04 +0400129 return capScore
130}
131
132// scorePriorityModelMatch scores based on priority and model sophistication
133func (a *AutoAssigner) scorePriorityModelMatch(priority tm.TaskPriority, model string, score *AssignmentScore) float64 {
134 priorityScore := 0.0
iomodo50598c62025-07-27 22:06:32 +0400135
user5a7d60d2025-07-27 21:22:04 +0400136 // 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 }
iomodo50598c62025-07-27 22:06:32 +0400141
user5a7d60d2025-07-27 21:22:04 +0400142 // 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 }
iomodo50598c62025-07-27 22:06:32 +0400147
user5a7d60d2025-07-27 21:22:04 +0400148 return priorityScore
149}
150
151// scoreExplicitMention scores based on explicit agent/role mentions
152func (a *AutoAssigner) scoreExplicitMention(agentName, agentRole, taskText string, score *AssignmentScore) float64 {
153 mentionScore := 0.0
iomodo50598c62025-07-27 22:06:32 +0400154
user5a7d60d2025-07-27 21:22:04 +0400155 // 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 }
iomodo50598c62025-07-27 22:06:32 +0400160
user5a7d60d2025-07-27 21:22:04 +0400161 // 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 }
iomodo50598c62025-07-27 22:06:32 +0400167
user5a7d60d2025-07-27 21:22:04 +0400168 return mentionScore
169}
170
171// extractKeywords extracts relevant keywords from task text
172func 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 })
iomodo50598c62025-07-27 22:06:32 +0400177
user5a7d60d2025-07-27 21:22:04 +0400178 // 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 }
iomodo50598c62025-07-27 22:06:32 +0400186
user5a7d60d2025-07-27 21:22:04 +0400187 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 }
iomodo50598c62025-07-27 22:06:32 +0400194
user5a7d60d2025-07-27 21:22:04 +0400195 return keywords
196}
197
198// ValidateAssignment checks if an assignment is valid
199func (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 }
iomodo50598c62025-07-27 22:06:32 +0400206
user5a7d60d2025-07-27 21:22:04 +0400207 return fmt.Errorf("agent '%s' not found", agentName)
208}
209
210// GetAgentCapabilities returns the capabilities of a specific agent
211func (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 }
iomodo50598c62025-07-27 22:06:32 +0400217
user5a7d60d2025-07-27 21:22:04 +0400218 return nil, fmt.Errorf("agent '%s' not found", agentName)
219}
220
221// GetRecommendationExplanation returns a human-readable explanation for assignment
222func (a *AutoAssigner) GetRecommendationExplanation(task *tm.Task, agentName string) string {
223 recommendations := a.GetAssignmentRecommendations(task)
iomodo50598c62025-07-27 22:06:32 +0400224
user5a7d60d2025-07-27 21:22:04 +0400225 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 }
iomodo50598c62025-07-27 22:06:32 +0400230
user5a7d60d2025-07-27 21:22:04 +0400231 reasons := strings.Join(rec.Reasons, ", ")
232 return fmt.Sprintf("Agent %s assigned (score: %.1f) because: %s", agentName, rec.Score, reasons)
233 }
234 }
iomodo50598c62025-07-27 22:06:32 +0400235
user5a7d60d2025-07-27 21:22:04 +0400236 return fmt.Sprintf("Agent %s assigned (manual override)", agentName)
iomodo50598c62025-07-27 22:06:32 +0400237}