blob: ad397ff96be942f15553a881852affed10db1dc9 [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
30// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
31func (s *SubtaskService) AnalyzeTaskForSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
32 prompt := s.buildSubtaskAnalysisPrompt(task)
33
34 req := llm.ChatCompletionRequest{
35 Model: "gpt-4",
36 Messages: []llm.Message{
37 {
38 Role: llm.RoleSystem,
39 Content: s.getSubtaskAnalysisSystemPrompt(),
40 },
41 {
42 Role: llm.RoleUser,
43 Content: prompt,
44 },
45 },
46 MaxTokens: &[]int{4000}[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 analysis 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 analysis, err := s.parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID)
61 if err != nil {
62 return nil, fmt.Errorf("failed to parse LLM response: %w", err)
63 }
64
65 return analysis, nil
66}
67
68// getSubtaskAnalysisSystemPrompt returns the system prompt for subtask analysis
69func (s *SubtaskService) getSubtaskAnalysisSystemPrompt() string {
70 availableRoles := strings.Join(s.agentRoles, ", ")
71
72 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.
73
74Available team roles: %s
75
76When analyzing a task, you should:
771. Understand the task requirements and scope
782. Break it down into logical, manageable subtasks
793. Assign each subtask to the most appropriate team role
804. Estimate effort and identify dependencies
815. Provide a clear execution strategy
82
83Respond with a JSON object in this exact format:
84{
85 "analysis_summary": "Brief analysis of the task and approach",
86 "subtasks": [
87 {
88 "title": "Subtask title",
89 "description": "Detailed description of what needs to be done",
90 "priority": "high|medium|low",
91 "assigned_to": "role_name",
92 "estimated_hours": 8,
93 "dependencies": ["subtask_index_1", "subtask_index_2"]
94 }
95 ],
96 "recommended_approach": "High-level strategy for executing these subtasks",
97 "estimated_total_hours": 40,
98 "risk_assessment": "Potential risks and mitigation strategies"
99}
100
101Only use the available team roles for assignment. Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles)
102}
103
104// buildSubtaskAnalysisPrompt creates the user prompt for LLM analysis
105func (s *SubtaskService) buildSubtaskAnalysisPrompt(task *tm.Task) string {
106 return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
107
108**Task Title:** %s
109
110**Description:** %s
111
112**Priority:** %s
113
114**Current Status:** %s
115
116Please analyze this task and provide a detailed breakdown into subtasks. Consider:
117- Technical complexity and requirements
118- Logical task dependencies
119- Appropriate skill sets needed for each subtask
120- Risk factors and potential blockers
121- Estimated effort for each component
122
123Provide the analysis in the JSON format specified in the system prompt.`,
124 task.Title,
125 task.Description,
126 task.Priority,
127 task.Status)
128}
129
130// parseSubtaskAnalysis parses the LLM response into a SubtaskAnalysis struct
131func (s *SubtaskService) parseSubtaskAnalysis(response string, parentTaskID string) (*tm.SubtaskAnalysis, error) {
132 // Try to extract JSON from the response (LLM might wrap it in markdown)
133 jsonStart := strings.Index(response, "{")
134 jsonEnd := strings.LastIndex(response, "}")
135
136 if jsonStart == -1 || jsonEnd == -1 {
137 return nil, fmt.Errorf("no JSON found in LLM response")
138 }
139
140 jsonStr := response[jsonStart : jsonEnd+1]
141
142 var rawAnalysis struct {
143 AnalysisSummary string `json:"analysis_summary"`
144 Subtasks []struct {
145 Title string `json:"title"`
146 Description string `json:"description"`
147 Priority string `json:"priority"`
148 AssignedTo string `json:"assigned_to"`
149 EstimatedHours int `json:"estimated_hours"`
150 Dependencies []string `json:"dependencies"`
151 } `json:"subtasks"`
152 RecommendedApproach string `json:"recommended_approach"`
153 EstimatedTotalHours int `json:"estimated_total_hours"`
154 RiskAssessment string `json:"risk_assessment"`
155 }
156
157 if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
158 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
159 }
160
161 // Convert to our types
162 analysis := &tm.SubtaskAnalysis{
163 ParentTaskID: parentTaskID,
164 AnalysisSummary: rawAnalysis.AnalysisSummary,
165 RecommendedApproach: rawAnalysis.RecommendedApproach,
166 EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
167 RiskAssessment: rawAnalysis.RiskAssessment,
168 }
169
170 // Convert subtasks
171 for _, st := range rawAnalysis.Subtasks {
172 priority := tm.PriorityMedium // default
173 switch strings.ToLower(st.Priority) {
174 case "high":
175 priority = tm.PriorityHigh
176 case "low":
177 priority = tm.PriorityLow
178 }
179
180 subtask := tm.SubtaskProposal{
181 Title: st.Title,
182 Description: st.Description,
183 Priority: priority,
184 AssignedTo: st.AssignedTo,
185 EstimatedHours: st.EstimatedHours,
186 Dependencies: st.Dependencies,
187 }
188
189 analysis.Subtasks = append(analysis.Subtasks, subtask)
190 }
191
192 // Validate agent assignments
193 if err := s.validateAgentAssignments(analysis); err != nil {
194 log.Printf("Warning: Invalid agent assignments: %v", err)
195 // Fix assignments by using first available role
196 s.fixAgentAssignments(analysis)
197 }
198
199 return analysis, nil
200}
201
202// validateAgentAssignments checks if all assigned roles are valid
203func (s *SubtaskService) validateAgentAssignments(analysis *tm.SubtaskAnalysis) error {
204 for i, subtask := range analysis.Subtasks {
205 if !s.isValidAgentRole(subtask.AssignedTo) {
206 return fmt.Errorf("subtask %d has invalid agent role: %s", i, subtask.AssignedTo)
207 }
208 }
209 return nil
210}
211
212// fixAgentAssignments fixes invalid agent assignments
213func (s *SubtaskService) fixAgentAssignments(analysis *tm.SubtaskAnalysis) {
214 defaultRole := "ceo" // fallback role
215 if len(s.agentRoles) > 0 {
216 defaultRole = s.agentRoles[0]
217 }
218
219 for i := range analysis.Subtasks {
220 if !s.isValidAgentRole(analysis.Subtasks[i].AssignedTo) {
221 analysis.Subtasks[i].AssignedTo = defaultRole
222 }
223 }
224}
225
226// isValidAgentRole checks if a role is in the available agent roles
227func (s *SubtaskService) isValidAgentRole(role string) bool {
228 for _, availableRole := range s.agentRoles {
229 if availableRole == role {
230 return true
231 }
232 }
233 return false
234}
235
236// GenerateSubtaskPR creates a PR with the proposed subtasks
237func (s *SubtaskService) GenerateSubtaskPR(ctx context.Context, analysis *tm.SubtaskAnalysis) (string, error) {
238 // Generate markdown content for the PR
239 prContent := s.generateSubtaskPRContent(analysis)
240
241 // This would typically create a Git branch and PR
242 // For now, we'll return a mock PR URL
243 prURL := fmt.Sprintf("https://github.com/example/repo/pull/subtasks-%s", analysis.ParentTaskID)
244
245 log.Printf("Generated subtask proposal PR: %s", prURL)
246 log.Printf("PR Content:\n%s", prContent)
247
248 return prURL, nil
249}
250
251// generateSubtaskPRContent creates markdown content for the subtask proposal PR
252func (s *SubtaskService) generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
253 var content strings.Builder
254
255 content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
256 content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
257 content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
258 content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
259
260 if analysis.RiskAssessment != "" {
261 content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
262 }
263
264 content.WriteString("## Proposed Subtasks\n\n")
265
266 for i, subtask := range analysis.Subtasks {
267 content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
268 content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
269 content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
270 content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
271
272 if len(subtask.Dependencies) > 0 {
273 deps := strings.Join(subtask.Dependencies, ", ")
274 content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
275 }
276
277 content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
278 }
279
280 content.WriteString("---\n")
281 content.WriteString("*Generated by Staff AI Agent System*\n\n")
282 content.WriteString("**Instructions:**\n")
283 content.WriteString("- Review the proposed subtasks\n")
284 content.WriteString("- Approve or request changes\n")
285 content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
286
287 return content.String()
288}
289
290// Close cleans up the service
291func (s *SubtaskService) Close() error {
292 if s.llmProvider != nil {
293 return s.llmProvider.Close()
294 }
295 return nil
296}