| package task |
| |
| import ( |
| "fmt" |
| "sort" |
| "strings" |
| |
| "github.com/iomodo/staff/config" |
| "github.com/iomodo/staff/tm" |
| ) |
| |
| // AutoAssigner handles intelligent task assignment to agents |
| type AutoAssigner struct { |
| agents []config.AgentConfig |
| } |
| |
| // NewAutoAssigner creates a new auto-assignment system |
| func NewAutoAssigner(agents []config.AgentConfig) *AutoAssigner { |
| return &AutoAssigner{ |
| agents: agents, |
| } |
| } |
| |
| // AssignmentScore represents how well an agent matches a task |
| type AssignmentScore struct { |
| AgentName string |
| Score float64 |
| Reasons []string |
| } |
| |
| // AssignTask automatically assigns a task to the best matching agent |
| func (a *AutoAssigner) AssignTask(task *tm.Task) (string, error) { |
| if len(a.agents) == 0 { |
| return "", fmt.Errorf("no agents available for assignment") |
| } |
| |
| scores := a.calculateScores(task) |
| if len(scores) == 0 { |
| return "", fmt.Errorf("no suitable agent found for task") |
| } |
| |
| // Sort by score (highest first) |
| sort.Slice(scores, func(i, j int) bool { |
| return scores[i].Score > scores[j].Score |
| }) |
| |
| bestMatch := scores[0] |
| if bestMatch.Score == 0 { |
| // No good match, assign to CEO as fallback |
| return "ceo", nil |
| } |
| |
| return bestMatch.AgentName, nil |
| } |
| |
| // GetAssignmentRecommendations returns ranked recommendations for task assignment |
| func (a *AutoAssigner) GetAssignmentRecommendations(task *tm.Task) []AssignmentScore { |
| scores := a.calculateScores(task) |
| |
| // Sort by score (highest first) |
| sort.Slice(scores, func(i, j int) bool { |
| return scores[i].Score > scores[j].Score |
| }) |
| |
| return scores |
| } |
| |
| // calculateScores calculates assignment scores for all agents |
| func (a *AutoAssigner) calculateScores(task *tm.Task) []AssignmentScore { |
| scores := make([]AssignmentScore, 0, len(a.agents)) |
| |
| taskText := strings.ToLower(task.Title + " " + task.Description) |
| taskKeywords := extractKeywords(taskText) |
| |
| for _, agent := range a.agents { |
| score := &AssignmentScore{ |
| AgentName: agent.Name, |
| Score: 0, |
| Reasons: make([]string, 0), |
| } |
| |
| // Score based on task type keywords |
| score.Score += a.scoreTaskTypes(agent.TaskTypes, taskKeywords, score) |
| |
| // Score based on capabilities |
| score.Score += a.scoreCapabilities(agent.Capabilities, taskKeywords, score) |
| |
| // Score based on task priority and agent model |
| score.Score += a.scorePriorityModelMatch(task.Priority, agent.Model, score) |
| |
| // Score based on explicit agent mention in task |
| score.Score += a.scoreExplicitMention(agent.Name, agent.Role, taskText, score) |
| |
| scores = append(scores, *score) |
| } |
| |
| return scores |
| } |
| |
| // scoreTaskTypes scores based on task type matching |
| func (a *AutoAssigner) scoreTaskTypes(agentTypes []string, taskKeywords []string, score *AssignmentScore) float64 { |
| typeScore := 0.0 |
| |
| for _, agentType := range agentTypes { |
| for _, keyword := range taskKeywords { |
| if strings.Contains(keyword, agentType) || strings.Contains(agentType, keyword) { |
| typeScore += 3.0 |
| score.Reasons = append(score.Reasons, fmt.Sprintf("matches task type: %s", agentType)) |
| } |
| } |
| } |
| |
| return typeScore |
| } |
| |
| // scoreCapabilities scores based on capability matching |
| func (a *AutoAssigner) scoreCapabilities(capabilities []string, taskKeywords []string, score *AssignmentScore) float64 { |
| capScore := 0.0 |
| |
| for _, capability := range capabilities { |
| for _, keyword := range taskKeywords { |
| if strings.Contains(keyword, capability) || strings.Contains(capability, keyword) { |
| capScore += 2.0 |
| score.Reasons = append(score.Reasons, fmt.Sprintf("has capability: %s", capability)) |
| } |
| } |
| } |
| |
| return capScore |
| } |
| |
| // scorePriorityModelMatch scores based on priority and model sophistication |
| func (a *AutoAssigner) scorePriorityModelMatch(priority tm.TaskPriority, model string, score *AssignmentScore) float64 { |
| priorityScore := 0.0 |
| |
| // High priority tasks prefer more capable models |
| if priority == tm.PriorityHigh && strings.Contains(model, "gpt-4") { |
| priorityScore += 1.0 |
| score.Reasons = append(score.Reasons, "high priority task matches advanced model") |
| } |
| |
| // Medium/low priority can use efficient models |
| if priority != tm.PriorityHigh && strings.Contains(model, "gpt-3.5") { |
| priorityScore += 0.5 |
| score.Reasons = append(score.Reasons, "priority matches model efficiency") |
| } |
| |
| return priorityScore |
| } |
| |
| // scoreExplicitMention scores based on explicit agent/role mentions |
| func (a *AutoAssigner) scoreExplicitMention(agentName, agentRole, taskText string, score *AssignmentScore) float64 { |
| mentionScore := 0.0 |
| |
| // Check for explicit agent name mention |
| if strings.Contains(taskText, agentName) { |
| mentionScore += 5.0 |
| score.Reasons = append(score.Reasons, "explicitly mentioned by name") |
| } |
| |
| // Check for role mention |
| roleLower := strings.ToLower(agentRole) |
| if strings.Contains(taskText, roleLower) { |
| mentionScore += 4.0 |
| score.Reasons = append(score.Reasons, "role mentioned in task") |
| } |
| |
| return mentionScore |
| } |
| |
| // extractKeywords extracts relevant keywords from task text |
| func extractKeywords(text string) []string { |
| // Simple keyword extraction - split by common delimiters |
| words := strings.FieldsFunc(text, func(c rune) bool { |
| return c == ' ' || c == ',' || c == '.' || c == ':' || c == ';' || c == '\n' || c == '\t' |
| }) |
| |
| // Filter out common stop words and short words |
| stopWords := map[string]bool{ |
| "the": true, "a": true, "an": true, "and": true, "or": true, "but": true, |
| "in": true, "on": true, "at": true, "to": true, "for": true, "of": true, |
| "with": true, "by": true, "is": true, "are": true, "was": true, "were": true, |
| "be": true, "been": true, "have": true, "has": true, "had": true, "do": true, |
| "does": true, "did": true, "will": true, "would": true, "could": true, "should": true, |
| } |
| |
| keywords := make([]string, 0) |
| for _, word := range words { |
| word = strings.ToLower(strings.TrimSpace(word)) |
| if len(word) > 2 && !stopWords[word] { |
| keywords = append(keywords, word) |
| } |
| } |
| |
| return keywords |
| } |
| |
| // ValidateAssignment checks if an assignment is valid |
| func (a *AutoAssigner) ValidateAssignment(agentName string, task *tm.Task) error { |
| // Check if agent exists |
| for _, agent := range a.agents { |
| if agent.Name == agentName { |
| return nil |
| } |
| } |
| |
| return fmt.Errorf("agent '%s' not found", agentName) |
| } |
| |
| // GetAgentCapabilities returns the capabilities of a specific agent |
| func (a *AutoAssigner) GetAgentCapabilities(agentName string) ([]string, error) { |
| for _, agent := range a.agents { |
| if agent.Name == agentName { |
| return agent.Capabilities, nil |
| } |
| } |
| |
| return nil, fmt.Errorf("agent '%s' not found", agentName) |
| } |
| |
| // GetRecommendationExplanation returns a human-readable explanation for assignment |
| func (a *AutoAssigner) GetRecommendationExplanation(task *tm.Task, agentName string) string { |
| recommendations := a.GetAssignmentRecommendations(task) |
| |
| for _, rec := range recommendations { |
| if rec.AgentName == agentName { |
| if len(rec.Reasons) == 0 { |
| return fmt.Sprintf("Agent %s assigned (score: %.1f)", agentName, rec.Score) |
| } |
| |
| reasons := strings.Join(rec.Reasons, ", ") |
| return fmt.Sprintf("Agent %s assigned (score: %.1f) because: %s", agentName, rec.Score, reasons) |
| } |
| } |
| |
| return fmt.Sprintf("Agent %s assigned (manual override)", agentName) |
| } |