package composer import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/rs/zerolog/log" ) // OllamaClient provides LLM integration with Ollama instances type OllamaClient struct { baseURL string httpClient *http.Client model string } // OllamaRequest represents a request to the Ollama API type OllamaRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` Stream bool `json:"stream"` } // OllamaResponse represents a response from the Ollama API type OllamaResponse struct { Model string `json:"model"` CreatedAt time.Time `json:"created_at"` Response string `json:"response"` Done bool `json:"done"` Context []int `json:"context,omitempty"` TotalDuration int64 `json:"total_duration,omitempty"` LoadDuration int64 `json:"load_duration,omitempty"` PromptEvalCount int `json:"prompt_eval_count,omitempty"` PromptEvalDuration int64 `json:"prompt_eval_duration,omitempty"` EvalCount int `json:"eval_count,omitempty"` EvalDuration int64 `json:"eval_duration,omitempty"` } // NewOllamaClient creates a new Ollama client with fallback endpoints func NewOllamaClient(model string) *OllamaClient { // Default Ollama endpoints from cluster endpoints := []string{ "http://192.168.1.27:11434", "http://192.168.1.72:11434", "http://192.168.1.113:11434", "http://192.168.1.106:11434", } // Try to find a working endpoint baseURL := endpoints[0] // Default to first endpoint httpClient := &http.Client{ Timeout: 30 * time.Second, } // Quick health check to find working endpoint for _, endpoint := range endpoints { resp, err := httpClient.Get(endpoint + "/api/tags") if err == nil && resp.StatusCode == 200 { baseURL = endpoint resp.Body.Close() break } if resp != nil { resp.Body.Close() } } log.Info(). Str("base_url", baseURL). Str("model", model). Msg("Initialized Ollama client") return &OllamaClient{ baseURL: baseURL, httpClient: httpClient, model: model, } } // Generate sends a prompt to Ollama and returns the response func (c *OllamaClient) Generate(ctx context.Context, prompt string) (string, error) { reqBody := OllamaRequest{ Model: c.model, Prompt: prompt, Stream: false, } jsonData, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/generate", bytes.NewBuffer(jsonData)) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") startTime := time.Now() resp, err := c.httpClient.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("ollama API returned status %d", resp.StatusCode) } var ollamaResp OllamaResponse if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil { return "", fmt.Errorf("failed to decode response: %w", err) } duration := time.Since(startTime) log.Debug(). Str("model", c.model). Dur("duration", duration). Int("eval_count", ollamaResp.EvalCount). Msg("Ollama generation completed") return ollamaResp.Response, nil } // TaskClassificationPrompt builds a prompt for task classification func (c *OllamaClient) BuildTaskClassificationPrompt(input *TaskAnalysisInput) string { var prompt strings.Builder prompt.WriteString("You are an expert software project manager analyzing development tasks. ") prompt.WriteString("Classify the following task and provide analysis in JSON format.\n\n") prompt.WriteString("Task Details:\n") prompt.WriteString(fmt.Sprintf("Title: %s\n", input.Title)) prompt.WriteString(fmt.Sprintf("Description: %s\n", input.Description)) if len(input.Requirements) > 0 { prompt.WriteString(fmt.Sprintf("Requirements: %s\n", strings.Join(input.Requirements, ", "))) } if len(input.TechStack) > 0 { prompt.WriteString(fmt.Sprintf("Tech Stack: %s\n", strings.Join(input.TechStack, ", "))) } prompt.WriteString(fmt.Sprintf("Priority: %s\n\n", input.Priority)) prompt.WriteString("Analyze this task and respond with valid JSON in this EXACT format:\n") prompt.WriteString("{\n") prompt.WriteString(" \"task_type\": \"feature_development\",\n") prompt.WriteString(" \"complexity_score\": 0.7,\n") prompt.WriteString(" \"primary_domains\": [\"backend\", \"api\"],\n") prompt.WriteString(" \"secondary_domains\": [\"testing\"],\n") prompt.WriteString(" \"estimated_duration_hours\": 8,\n") prompt.WriteString(" \"risk_level\": \"medium\",\n") prompt.WriteString(" \"required_experience\": \"intermediate\"\n") prompt.WriteString("}\n\n") prompt.WriteString("Task types: feature_development, bug_fix, refactoring, security, integration, optimization, maintenance\n") prompt.WriteString("Complexity: number between 0.1-1.0\n") prompt.WriteString("Duration: integer between 1-40\n") prompt.WriteString("Risk: minimal, low, medium, high\n") prompt.WriteString("Experience: junior, intermediate, senior\n\n") prompt.WriteString("Respond ONLY with valid JSON, no markdown, no other text.") return prompt.String() } // SkillAnalysisPrompt builds a prompt for skill requirement analysis func (c *OllamaClient) BuildSkillAnalysisPrompt(input *TaskAnalysisInput, classification *TaskClassification) string { var prompt strings.Builder prompt.WriteString("You are an expert technical recruiter analyzing skill requirements for software development tasks. ") prompt.WriteString("Analyze the task and provide detailed skill requirements in JSON format.\n\n") prompt.WriteString("Task Details:\n") prompt.WriteString(fmt.Sprintf("Title: %s\n", input.Title)) prompt.WriteString(fmt.Sprintf("Description: %s\n", input.Description)) prompt.WriteString(fmt.Sprintf("Task Type: %s\n", classification.TaskType)) prompt.WriteString(fmt.Sprintf("Complexity: %.1f\n", classification.ComplexityScore)) prompt.WriteString(fmt.Sprintf("Primary Domains: %s\n", strings.Join(classification.PrimaryDomains, ", "))) if len(input.Requirements) > 0 { prompt.WriteString(fmt.Sprintf("Requirements: %s\n", strings.Join(input.Requirements, ", "))) } if len(input.TechStack) > 0 { prompt.WriteString(fmt.Sprintf("Tech Stack: %s\n", strings.Join(input.TechStack, ", "))) } prompt.WriteString("\nAnalyze and respond with JSON containing:\n") prompt.WriteString("{\n") prompt.WriteString(" \"critical_skills\": [\n") prompt.WriteString(" {\n") prompt.WriteString(" \"domain\": \"skill domain name\",\n") prompt.WriteString(" \"min_proficiency\": 0.1-1.0,\n") prompt.WriteString(" \"weight\": 0.1-1.0,\n") prompt.WriteString(" \"critical\": true\n") prompt.WriteString(" }\n") prompt.WriteString(" ],\n") prompt.WriteString(" \"desirable_skills\": [\n") prompt.WriteString(" {\n") prompt.WriteString(" \"domain\": \"skill domain name\",\n") prompt.WriteString(" \"min_proficiency\": 0.1-1.0,\n") prompt.WriteString(" \"weight\": 0.1-1.0,\n") prompt.WriteString(" \"critical\": false\n") prompt.WriteString(" }\n") prompt.WriteString(" ],\n") prompt.WriteString(" \"total_skill_count\": 0\n") prompt.WriteString("}\n\n") prompt.WriteString("Focus on:\n") prompt.WriteString("- Programming languages and frameworks\n") prompt.WriteString("- Domain expertise (backend, frontend, devops, etc.)\n") prompt.WriteString("- Tools and technologies\n") prompt.WriteString("- Soft skills relevant to the task type\n\n") prompt.WriteString("Respond ONLY with valid JSON, no other text.") return prompt.String() } // ParseTaskClassificationResponse parses LLM response into TaskClassification func (c *OllamaClient) ParseTaskClassificationResponse(response string) (*TaskClassification, error) { // Clean the response - remove markdown code blocks if present response = strings.TrimSpace(response) response = strings.TrimPrefix(response, "```json") response = strings.TrimPrefix(response, "```") response = strings.TrimSuffix(response, "```") response = strings.TrimSpace(response) var result struct { TaskType string `json:"task_type"` ComplexityScore float64 `json:"complexity_score"` PrimaryDomains []string `json:"primary_domains"` SecondaryDomains []string `json:"secondary_domains"` EstimatedDuration int `json:"estimated_duration_hours"` RiskLevel string `json:"risk_level"` RequiredExperience string `json:"required_experience"` } if err := json.Unmarshal([]byte(response), &result); err != nil { return nil, fmt.Errorf("failed to parse classification response: %w", err) } // Convert task type string to TaskType var taskType TaskType switch result.TaskType { case "feature_development": taskType = TaskTypeFeatureDevelopment case "bug_fix": taskType = TaskTypeBugFix case "refactoring": taskType = TaskTypeRefactoring case "security": taskType = TaskTypeSecurity case "integration": taskType = TaskTypeIntegration case "optimization": taskType = TaskTypeOptimization case "maintenance": taskType = TaskTypeMaintenance default: taskType = TaskTypeFeatureDevelopment // Default fallback } classification := &TaskClassification{ TaskType: taskType, ComplexityScore: result.ComplexityScore, PrimaryDomains: result.PrimaryDomains, SecondaryDomains: result.SecondaryDomains, EstimatedDuration: result.EstimatedDuration, RiskLevel: result.RiskLevel, RequiredExperience: result.RequiredExperience, } return classification, nil } // ParseSkillRequirementsResponse parses LLM response into SkillRequirements func (c *OllamaClient) ParseSkillRequirementsResponse(response string) (*SkillRequirements, error) { // Clean the response - remove markdown code blocks if present response = strings.TrimSpace(response) response = strings.TrimPrefix(response, "```json") response = strings.TrimPrefix(response, "```") response = strings.TrimSuffix(response, "```") response = strings.TrimSpace(response) var result struct { CriticalSkills []struct { Domain string `json:"domain"` MinProficiency float64 `json:"min_proficiency"` Weight float64 `json:"weight"` Critical bool `json:"critical"` } `json:"critical_skills"` DesirableSkills []struct { Domain string `json:"domain"` MinProficiency float64 `json:"min_proficiency"` Weight float64 `json:"weight"` Critical bool `json:"critical"` } `json:"desirable_skills"` TotalSkillCount int `json:"total_skill_count"` } if err := json.Unmarshal([]byte(response), &result); err != nil { return nil, fmt.Errorf("failed to parse skill requirements response: %w", err) } // Convert to SkillRequirements format critical := make([]SkillRequirement, len(result.CriticalSkills)) for i, skill := range result.CriticalSkills { critical[i] = SkillRequirement{ Domain: skill.Domain, MinProficiency: skill.MinProficiency, Weight: skill.Weight, Critical: skill.Critical, } } desirable := make([]SkillRequirement, len(result.DesirableSkills)) for i, skill := range result.DesirableSkills { desirable[i] = SkillRequirement{ Domain: skill.Domain, MinProficiency: skill.MinProficiency, Weight: skill.Weight, Critical: skill.Critical, } } skillRequirements := &SkillRequirements{ CriticalSkills: critical, DesirableSkills: desirable, TotalSkillCount: len(critical) + len(desirable), } return skillRequirements, nil }