blob: 710dfafe85771817e1612bd7891c89f5f36190fa [file] [log] [blame]
iomodod9ff8da2025-07-28 11:42:22 +04001package subtasks
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log"
8 "strings"
9
10 "github.com/iomodo/staff/llm"
11 "github.com/iomodo/staff/tm"
12)
13
14// SubtaskService handles subtask generation and management
15type SubtaskService struct {
16 llmProvider llm.LLMProvider
17 taskManager tm.TaskManager
18 agentRoles []string // Available agent roles for assignment
19}
20
21// NewSubtaskService creates a new subtask service
22func NewSubtaskService(provider llm.LLMProvider, taskManager tm.TaskManager, agentRoles []string) *SubtaskService {
23 return &SubtaskService{
24 llmProvider: provider,
25 taskManager: taskManager,
26 agentRoles: agentRoles,
27 }
28}
29
iomodo5c99a442025-07-28 14:23:52 +040030// ShouldGenerateSubtasks asks LLM whether a task needs subtasks based on existing agents
31func (s *SubtaskService) ShouldGenerateSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskDecision, error) {
32 prompt := s.buildSubtaskDecisionPrompt(task)
33
34 req := llm.ChatCompletionRequest{
35 Model: "gpt-4",
36 Messages: []llm.Message{
37 {
38 Role: llm.RoleSystem,
39 Content: s.getSubtaskDecisionSystemPrompt(),
40 },
41 {
42 Role: llm.RoleUser,
43 Content: prompt,
44 },
45 },
46 MaxTokens: &[]int{1000}[0],
47 Temperature: &[]float64{0.3}[0],
48 }
49
50 resp, err := s.llmProvider.ChatCompletion(ctx, req)
51 if err != nil {
52 return nil, fmt.Errorf("LLM decision failed: %w", err)
53 }
54
55 if len(resp.Choices) == 0 {
56 return nil, fmt.Errorf("no response from LLM")
57 }
58
59 // Parse the LLM response
60 decision, err := s.parseSubtaskDecision(resp.Choices[0].Message.Content)
61 if err != nil {
62 return nil, fmt.Errorf("failed to parse LLM decision: %w", err)
63 }
64
65 return decision, nil
66}
67
iomodod9ff8da2025-07-28 11:42:22 +040068// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
69func (s *SubtaskService) AnalyzeTaskForSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
70 prompt := s.buildSubtaskAnalysisPrompt(task)
71
72 req := llm.ChatCompletionRequest{
73 Model: "gpt-4",
74 Messages: []llm.Message{
75 {
76 Role: llm.RoleSystem,
77 Content: s.getSubtaskAnalysisSystemPrompt(),
78 },
79 {
80 Role: llm.RoleUser,
81 Content: prompt,
82 },
83 },
84 MaxTokens: &[]int{4000}[0],
85 Temperature: &[]float64{0.3}[0],
86 }
87
88 resp, err := s.llmProvider.ChatCompletion(ctx, req)
89 if err != nil {
90 return nil, fmt.Errorf("LLM analysis failed: %w", err)
91 }
92
93 if len(resp.Choices) == 0 {
94 return nil, fmt.Errorf("no response from LLM")
95 }
96
97 // Parse the LLM response
98 analysis, err := s.parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID)
99 if err != nil {
100 return nil, fmt.Errorf("failed to parse LLM response: %w", err)
101 }
102
103 return analysis, nil
104}
105
iomodo5c99a442025-07-28 14:23:52 +0400106// getSubtaskDecisionSystemPrompt returns the system prompt for subtask decision
107func (s *SubtaskService) getSubtaskDecisionSystemPrompt() string {
108 availableRoles := strings.Join(s.agentRoles, ", ")
109
110 return fmt.Sprintf(`You are an expert project manager and task analyst. Your job is to determine whether a task needs to be broken down into subtasks.
111
112Currently available team roles and their capabilities: %s
113
114When evaluating a task, consider:
1151. Task complexity and scope
1162. Whether multiple specialized skills are needed
1173. If the task can be completed by a single agent with current capabilities
1184. Whether new agent roles might be needed for specialized skills
119
120Respond with a JSON object in this exact format:
121{
122 "needs_subtasks": true/false,
123 "reasoning": "Clear explanation of why subtasks are or aren't needed",
124 "complexity_score": 5,
125 "required_skills": ["skill1", "skill2", "skill3"]
126}
127
128Complexity score should be 1-10 where:
129- 1-3: Simple tasks that can be handled by one agent
130- 4-6: Moderate complexity, might benefit from subtasks
131- 7-10: Complex tasks that definitely need breaking down
132
133Required skills should list all technical/domain skills needed to complete the task.`, availableRoles)
134}
135
iomodod9ff8da2025-07-28 11:42:22 +0400136// getSubtaskAnalysisSystemPrompt returns the system prompt for subtask analysis
137func (s *SubtaskService) getSubtaskAnalysisSystemPrompt() string {
138 availableRoles := strings.Join(s.agentRoles, ", ")
139
140 return fmt.Sprintf(`You are an expert project manager and technical architect. Your job is to analyze complex tasks and break them down into well-defined subtasks that can be assigned to specialized team members.
141
iomodo5c99a442025-07-28 14:23:52 +0400142Currently available team roles: %s
iomodod9ff8da2025-07-28 11:42:22 +0400143
144When analyzing a task, you should:
1451. Understand the task requirements and scope
1462. Break it down into logical, manageable subtasks
iomodo5c99a442025-07-28 14:23:52 +04001473. Assign each subtask to the most appropriate team role OR propose creating new agents
iomodod9ff8da2025-07-28 11:42:22 +04001484. Estimate effort and identify dependencies
1495. Provide a clear execution strategy
150
iomodo5c99a442025-07-28 14:23:52 +0400151If you need specialized skills not covered by existing roles, propose new agent creation.
152
iomodod9ff8da2025-07-28 11:42:22 +0400153Respond with a JSON object in this exact format:
154{
155 "analysis_summary": "Brief analysis of the task and approach",
156 "subtasks": [
157 {
158 "title": "Subtask title",
159 "description": "Detailed description of what needs to be done",
160 "priority": "high|medium|low",
161 "assigned_to": "role_name",
162 "estimated_hours": 8,
iomodo5c99a442025-07-28 14:23:52 +0400163 "dependencies": ["subtask_index_1", "subtask_index_2"],
164 "required_skills": ["skill1", "skill2"]
165 }
166 ],
167 "agent_creations": [
168 {
169 "role": "new_role_name",
170 "skills": ["specialized_skill1", "specialized_skill2"],
171 "description": "Description of what this agent does",
172 "justification": "Why this new agent is needed"
iomodod9ff8da2025-07-28 11:42:22 +0400173 }
174 ],
175 "recommended_approach": "High-level strategy for executing these subtasks",
176 "estimated_total_hours": 40,
177 "risk_assessment": "Potential risks and mitigation strategies"
178}
179
iomodo5c99a442025-07-28 14:23:52 +0400180For existing roles, use: %s
181For new agents, propose appropriate role names and skill sets.
182Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles, availableRoles)
183}
184
185// buildSubtaskDecisionPrompt creates the user prompt for subtask decision
186func (s *SubtaskService) buildSubtaskDecisionPrompt(task *tm.Task) string {
187 return fmt.Sprintf(`Please evaluate whether the following task needs to be broken down into subtasks:
188
189**Task Title:** %s
190
191**Description:** %s
192
193**Priority:** %s
194
195**Current Status:** %s
196
197Consider:
198- Can this be completed by a single agent with existing capabilities?
199- Does it require multiple specialized skills?
200- Is the scope too large for one person?
201- Are there logical components that could be parallelized?
202
203Provide your decision in the JSON format specified in the system prompt.`,
204 task.Title,
205 task.Description,
206 task.Priority,
207 task.Status)
iomodod9ff8da2025-07-28 11:42:22 +0400208}
209
210// buildSubtaskAnalysisPrompt creates the user prompt for LLM analysis
211func (s *SubtaskService) buildSubtaskAnalysisPrompt(task *tm.Task) string {
212 return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
213
214**Task Title:** %s
215
216**Description:** %s
217
218**Priority:** %s
219
220**Current Status:** %s
221
222Please analyze this task and provide a detailed breakdown into subtasks. Consider:
223- Technical complexity and requirements
224- Logical task dependencies
225- Appropriate skill sets needed for each subtask
226- Risk factors and potential blockers
227- Estimated effort for each component
228
229Provide the analysis in the JSON format specified in the system prompt.`,
230 task.Title,
231 task.Description,
232 task.Priority,
233 task.Status)
234}
235
iomodo5c99a442025-07-28 14:23:52 +0400236// parseSubtaskDecision parses the LLM response into a SubtaskDecision struct
237func (s *SubtaskService) parseSubtaskDecision(response string) (*tm.SubtaskDecision, error) {
238 // Try to extract JSON from the response
239 jsonStart := strings.Index(response, "{")
240 jsonEnd := strings.LastIndex(response, "}")
241
242 if jsonStart == -1 || jsonEnd == -1 {
243 return nil, fmt.Errorf("no JSON found in LLM response")
244 }
245
246 jsonStr := response[jsonStart : jsonEnd+1]
247
248 var decision tm.SubtaskDecision
249 if err := json.Unmarshal([]byte(jsonStr), &decision); err != nil {
250 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
251 }
252
253 return &decision, nil
254}
255
iomodod9ff8da2025-07-28 11:42:22 +0400256// parseSubtaskAnalysis parses the LLM response into a SubtaskAnalysis struct
257func (s *SubtaskService) parseSubtaskAnalysis(response string, parentTaskID string) (*tm.SubtaskAnalysis, error) {
258 // Try to extract JSON from the response (LLM might wrap it in markdown)
259 jsonStart := strings.Index(response, "{")
260 jsonEnd := strings.LastIndex(response, "}")
261
262 if jsonStart == -1 || jsonEnd == -1 {
263 return nil, fmt.Errorf("no JSON found in LLM response")
264 }
265
266 jsonStr := response[jsonStart : jsonEnd+1]
267
268 var rawAnalysis struct {
269 AnalysisSummary string `json:"analysis_summary"`
270 Subtasks []struct {
271 Title string `json:"title"`
272 Description string `json:"description"`
273 Priority string `json:"priority"`
274 AssignedTo string `json:"assigned_to"`
275 EstimatedHours int `json:"estimated_hours"`
276 Dependencies []string `json:"dependencies"`
iomodo5c99a442025-07-28 14:23:52 +0400277 RequiredSkills []string `json:"required_skills"`
iomodod9ff8da2025-07-28 11:42:22 +0400278 } `json:"subtasks"`
iomodo5c99a442025-07-28 14:23:52 +0400279 AgentCreations []tm.AgentCreationProposal `json:"agent_creations"`
iomodod9ff8da2025-07-28 11:42:22 +0400280 RecommendedApproach string `json:"recommended_approach"`
281 EstimatedTotalHours int `json:"estimated_total_hours"`
282 RiskAssessment string `json:"risk_assessment"`
283 }
284
285 if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
286 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
287 }
288
289 // Convert to our types
290 analysis := &tm.SubtaskAnalysis{
291 ParentTaskID: parentTaskID,
292 AnalysisSummary: rawAnalysis.AnalysisSummary,
iomodo5c99a442025-07-28 14:23:52 +0400293 AgentCreations: rawAnalysis.AgentCreations,
iomodod9ff8da2025-07-28 11:42:22 +0400294 RecommendedApproach: rawAnalysis.RecommendedApproach,
295 EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
296 RiskAssessment: rawAnalysis.RiskAssessment,
297 }
298
299 // Convert subtasks
300 for _, st := range rawAnalysis.Subtasks {
301 priority := tm.PriorityMedium // default
302 switch strings.ToLower(st.Priority) {
303 case "high":
304 priority = tm.PriorityHigh
305 case "low":
306 priority = tm.PriorityLow
307 }
308
309 subtask := tm.SubtaskProposal{
310 Title: st.Title,
311 Description: st.Description,
312 Priority: priority,
313 AssignedTo: st.AssignedTo,
314 EstimatedHours: st.EstimatedHours,
315 Dependencies: st.Dependencies,
iomodo5c99a442025-07-28 14:23:52 +0400316 RequiredSkills: st.RequiredSkills,
iomodod9ff8da2025-07-28 11:42:22 +0400317 }
318
319 analysis.Subtasks = append(analysis.Subtasks, subtask)
320 }
321
iomodo5c99a442025-07-28 14:23:52 +0400322 // Validate agent assignments and handle new agent creation
323 if err := s.validateAndHandleAgentAssignments(analysis); err != nil {
324 log.Printf("Warning during agent assignment handling: %v", err)
iomodod9ff8da2025-07-28 11:42:22 +0400325 }
326
327 return analysis, nil
328}
329
iomodo5c99a442025-07-28 14:23:52 +0400330// validateAndHandleAgentAssignments validates assignments and creates agent creation subtasks if needed
331func (s *SubtaskService) validateAndHandleAgentAssignments(analysis *tm.SubtaskAnalysis) error {
332 // Collect all agent roles that will be available (existing + proposed new ones)
333 availableRoles := make(map[string]bool)
334 for _, role := range s.agentRoles {
335 availableRoles[role] = true
336 }
337
338 // Add proposed new agent roles
339 for _, agentCreation := range analysis.AgentCreations {
340 availableRoles[agentCreation.Role] = true
341
342 // Create a subtask for agent creation
343 agentCreationSubtask := tm.SubtaskProposal{
344 Title: fmt.Sprintf("Create %s Agent", strings.Title(agentCreation.Role)),
345 Description: fmt.Sprintf("Create and configure a new %s agent with skills: %s. %s", agentCreation.Role, strings.Join(agentCreation.Skills, ", "), agentCreation.Justification),
346 Priority: tm.PriorityHigh, // Agent creation is high priority
347 AssignedTo: "ceo", // CEO creates new agents
348 EstimatedHours: 4, // Estimated time to set up new agent
349 Dependencies: []string{}, // No dependencies for agent creation
350 RequiredSkills: []string{"agent_configuration", "system_design"},
351 }
352
353 // Insert at the beginning so agent creation happens first
354 analysis.Subtasks = append([]tm.SubtaskProposal{agentCreationSubtask}, analysis.Subtasks...)
355
356 // Update dependencies to account for the new subtask at index 0
357 for i := 1; i < len(analysis.Subtasks); i++ {
358 for j, dep := range analysis.Subtasks[i].Dependencies {
359 // Convert dependency index and increment by 1
360 if depIndex := s.parseDependencyIndex(dep); depIndex >= 0 {
361 analysis.Subtasks[i].Dependencies[j] = fmt.Sprintf("%d", depIndex+1)
362 }
363 }
iomodod9ff8da2025-07-28 11:42:22 +0400364 }
365 }
iomodo5c99a442025-07-28 14:23:52 +0400366
367 // Now validate all assignments against available roles
iomodod9ff8da2025-07-28 11:42:22 +0400368 defaultRole := "ceo" // fallback role
369 if len(s.agentRoles) > 0 {
370 defaultRole = s.agentRoles[0]
371 }
372
373 for i := range analysis.Subtasks {
iomodo5c99a442025-07-28 14:23:52 +0400374 if !availableRoles[analysis.Subtasks[i].AssignedTo] {
375 log.Printf("Warning: Unknown agent role '%s' for subtask '%s', assigning to %s",
376 analysis.Subtasks[i].AssignedTo, analysis.Subtasks[i].Title, defaultRole)
iomodod9ff8da2025-07-28 11:42:22 +0400377 analysis.Subtasks[i].AssignedTo = defaultRole
378 }
379 }
iomodo5c99a442025-07-28 14:23:52 +0400380
381 return nil
382}
383
384// parseDependencyIndex parses a dependency string to an integer index
385func (s *SubtaskService) parseDependencyIndex(dep string) int {
386 var idx int
387 if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
388 return idx
389 }
390 return -1 // Invalid dependency format
iomodod9ff8da2025-07-28 11:42:22 +0400391}
392
393// isValidAgentRole checks if a role is in the available agent roles
394func (s *SubtaskService) isValidAgentRole(role string) bool {
395 for _, availableRole := range s.agentRoles {
396 if availableRole == role {
397 return true
398 }
399 }
400 return false
401}
402
403// GenerateSubtaskPR creates a PR with the proposed subtasks
404func (s *SubtaskService) GenerateSubtaskPR(ctx context.Context, analysis *tm.SubtaskAnalysis) (string, error) {
405 // Generate markdown content for the PR
406 prContent := s.generateSubtaskPRContent(analysis)
407
408 // This would typically create a Git branch and PR
409 // For now, we'll return a mock PR URL
410 prURL := fmt.Sprintf("https://github.com/example/repo/pull/subtasks-%s", analysis.ParentTaskID)
411
412 log.Printf("Generated subtask proposal PR: %s", prURL)
413 log.Printf("PR Content:\n%s", prContent)
414
415 return prURL, nil
416}
417
418// generateSubtaskPRContent creates markdown content for the subtask proposal PR
419func (s *SubtaskService) generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
420 var content strings.Builder
421
422 content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
423 content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
424 content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
425 content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
426
427 if analysis.RiskAssessment != "" {
428 content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
429 }
430
431 content.WriteString("## Proposed Subtasks\n\n")
432
433 for i, subtask := range analysis.Subtasks {
434 content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
435 content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
436 content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
437 content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
438
439 if len(subtask.Dependencies) > 0 {
440 deps := strings.Join(subtask.Dependencies, ", ")
441 content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
442 }
443
444 content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
445 }
446
447 content.WriteString("---\n")
448 content.WriteString("*Generated by Staff AI Agent System*\n\n")
449 content.WriteString("**Instructions:**\n")
450 content.WriteString("- Review the proposed subtasks\n")
451 content.WriteString("- Approve or request changes\n")
452 content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
453
454 return content.String()
455}
456
457// Close cleans up the service
458func (s *SubtaskService) Close() error {
459 if s.llmProvider != nil {
460 return s.llmProvider.Close()
461 }
462 return nil
463}