blob: ad397ff96be942f15553a881852affed10db1dc9 [file] [log] [blame]
package subtasks
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/iomodo/staff/llm"
"github.com/iomodo/staff/tm"
)
// SubtaskService handles subtask generation and management
type SubtaskService struct {
llmProvider llm.LLMProvider
taskManager tm.TaskManager
agentRoles []string // Available agent roles for assignment
}
// NewSubtaskService creates a new subtask service
func NewSubtaskService(provider llm.LLMProvider, taskManager tm.TaskManager, agentRoles []string) *SubtaskService {
return &SubtaskService{
llmProvider: provider,
taskManager: taskManager,
agentRoles: agentRoles,
}
}
// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
func (s *SubtaskService) AnalyzeTaskForSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
prompt := s.buildSubtaskAnalysisPrompt(task)
req := llm.ChatCompletionRequest{
Model: "gpt-4",
Messages: []llm.Message{
{
Role: llm.RoleSystem,
Content: s.getSubtaskAnalysisSystemPrompt(),
},
{
Role: llm.RoleUser,
Content: prompt,
},
},
MaxTokens: &[]int{4000}[0],
Temperature: &[]float64{0.3}[0],
}
resp, err := s.llmProvider.ChatCompletion(ctx, req)
if err != nil {
return nil, fmt.Errorf("LLM analysis failed: %w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("no response from LLM")
}
// Parse the LLM response
analysis, err := s.parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse LLM response: %w", err)
}
return analysis, nil
}
// getSubtaskAnalysisSystemPrompt returns the system prompt for subtask analysis
func (s *SubtaskService) getSubtaskAnalysisSystemPrompt() string {
availableRoles := strings.Join(s.agentRoles, ", ")
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.
Available team roles: %s
When analyzing a task, you should:
1. Understand the task requirements and scope
2. Break it down into logical, manageable subtasks
3. Assign each subtask to the most appropriate team role
4. Estimate effort and identify dependencies
5. Provide a clear execution strategy
Respond with a JSON object in this exact format:
{
"analysis_summary": "Brief analysis of the task and approach",
"subtasks": [
{
"title": "Subtask title",
"description": "Detailed description of what needs to be done",
"priority": "high|medium|low",
"assigned_to": "role_name",
"estimated_hours": 8,
"dependencies": ["subtask_index_1", "subtask_index_2"]
}
],
"recommended_approach": "High-level strategy for executing these subtasks",
"estimated_total_hours": 40,
"risk_assessment": "Potential risks and mitigation strategies"
}
Only use the available team roles for assignment. Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles)
}
// buildSubtaskAnalysisPrompt creates the user prompt for LLM analysis
func (s *SubtaskService) buildSubtaskAnalysisPrompt(task *tm.Task) string {
return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
**Task Title:** %s
**Description:** %s
**Priority:** %s
**Current Status:** %s
Please analyze this task and provide a detailed breakdown into subtasks. Consider:
- Technical complexity and requirements
- Logical task dependencies
- Appropriate skill sets needed for each subtask
- Risk factors and potential blockers
- Estimated effort for each component
Provide the analysis in the JSON format specified in the system prompt.`,
task.Title,
task.Description,
task.Priority,
task.Status)
}
// parseSubtaskAnalysis parses the LLM response into a SubtaskAnalysis struct
func (s *SubtaskService) parseSubtaskAnalysis(response string, parentTaskID string) (*tm.SubtaskAnalysis, error) {
// Try to extract JSON from the response (LLM might wrap it in markdown)
jsonStart := strings.Index(response, "{")
jsonEnd := strings.LastIndex(response, "}")
if jsonStart == -1 || jsonEnd == -1 {
return nil, fmt.Errorf("no JSON found in LLM response")
}
jsonStr := response[jsonStart : jsonEnd+1]
var rawAnalysis struct {
AnalysisSummary string `json:"analysis_summary"`
Subtasks []struct {
Title string `json:"title"`
Description string `json:"description"`
Priority string `json:"priority"`
AssignedTo string `json:"assigned_to"`
EstimatedHours int `json:"estimated_hours"`
Dependencies []string `json:"dependencies"`
} `json:"subtasks"`
RecommendedApproach string `json:"recommended_approach"`
EstimatedTotalHours int `json:"estimated_total_hours"`
RiskAssessment string `json:"risk_assessment"`
}
if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
// Convert to our types
analysis := &tm.SubtaskAnalysis{
ParentTaskID: parentTaskID,
AnalysisSummary: rawAnalysis.AnalysisSummary,
RecommendedApproach: rawAnalysis.RecommendedApproach,
EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
RiskAssessment: rawAnalysis.RiskAssessment,
}
// Convert subtasks
for _, st := range rawAnalysis.Subtasks {
priority := tm.PriorityMedium // default
switch strings.ToLower(st.Priority) {
case "high":
priority = tm.PriorityHigh
case "low":
priority = tm.PriorityLow
}
subtask := tm.SubtaskProposal{
Title: st.Title,
Description: st.Description,
Priority: priority,
AssignedTo: st.AssignedTo,
EstimatedHours: st.EstimatedHours,
Dependencies: st.Dependencies,
}
analysis.Subtasks = append(analysis.Subtasks, subtask)
}
// Validate agent assignments
if err := s.validateAgentAssignments(analysis); err != nil {
log.Printf("Warning: Invalid agent assignments: %v", err)
// Fix assignments by using first available role
s.fixAgentAssignments(analysis)
}
return analysis, nil
}
// validateAgentAssignments checks if all assigned roles are valid
func (s *SubtaskService) validateAgentAssignments(analysis *tm.SubtaskAnalysis) error {
for i, subtask := range analysis.Subtasks {
if !s.isValidAgentRole(subtask.AssignedTo) {
return fmt.Errorf("subtask %d has invalid agent role: %s", i, subtask.AssignedTo)
}
}
return nil
}
// fixAgentAssignments fixes invalid agent assignments
func (s *SubtaskService) fixAgentAssignments(analysis *tm.SubtaskAnalysis) {
defaultRole := "ceo" // fallback role
if len(s.agentRoles) > 0 {
defaultRole = s.agentRoles[0]
}
for i := range analysis.Subtasks {
if !s.isValidAgentRole(analysis.Subtasks[i].AssignedTo) {
analysis.Subtasks[i].AssignedTo = defaultRole
}
}
}
// isValidAgentRole checks if a role is in the available agent roles
func (s *SubtaskService) isValidAgentRole(role string) bool {
for _, availableRole := range s.agentRoles {
if availableRole == role {
return true
}
}
return false
}
// GenerateSubtaskPR creates a PR with the proposed subtasks
func (s *SubtaskService) GenerateSubtaskPR(ctx context.Context, analysis *tm.SubtaskAnalysis) (string, error) {
// Generate markdown content for the PR
prContent := s.generateSubtaskPRContent(analysis)
// This would typically create a Git branch and PR
// For now, we'll return a mock PR URL
prURL := fmt.Sprintf("https://github.com/example/repo/pull/subtasks-%s", analysis.ParentTaskID)
log.Printf("Generated subtask proposal PR: %s", prURL)
log.Printf("PR Content:\n%s", prContent)
return prURL, nil
}
// generateSubtaskPRContent creates markdown content for the subtask proposal PR
func (s *SubtaskService) generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
var content strings.Builder
content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
if analysis.RiskAssessment != "" {
content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
}
content.WriteString("## Proposed Subtasks\n\n")
for i, subtask := range analysis.Subtasks {
content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
if len(subtask.Dependencies) > 0 {
deps := strings.Join(subtask.Dependencies, ", ")
content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
}
content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
}
content.WriteString("---\n")
content.WriteString("*Generated by Staff AI Agent System*\n\n")
content.WriteString("**Instructions:**\n")
content.WriteString("- Review the proposed subtasks\n")
content.WriteString("- Approve or request changes\n")
content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
return content.String()
}
// Close cleans up the service
func (s *SubtaskService) Close() error {
if s.llmProvider != nil {
return s.llmProvider.Close()
}
return nil
}