package service import ( "encoding/json" "fmt" "strings" ) // parseJSONResponse attempts to extract a JSON object from LLM output. // LLMs often wrap JSON in markdown code blocks (```json ... ```), // so this function strips those wrappers before parsing. // Returns the parsed struct or an error if parsing fails. func parseJSONResponse(content string, target interface{}) error { // Strip markdown code block wrapper if present jsonStr := content jsonStr = strings.TrimSpace(jsonStr) // Remove ```json ... ``` wrapper if strings.HasPrefix(jsonStr, "```json") { jsonStr = strings.TrimPrefix(jsonStr, "```json") jsonStr = strings.TrimSuffix(jsonStr, "```") jsonStr = strings.TrimSpace(jsonStr) } else if strings.HasPrefix(jsonStr, "```") { jsonStr = strings.TrimPrefix(jsonStr, "```") jsonStr = strings.TrimSuffix(jsonStr, "```") jsonStr = strings.TrimSpace(jsonStr) } // Try direct JSON parse if err := json.Unmarshal([]byte(jsonStr), target); err != nil { // Try to find the first { ... } block in the content startIdx := strings.Index(jsonStr, "{") endIdx := strings.LastIndex(jsonStr, "}") if startIdx >= 0 && endIdx > startIdx { jsonBlock := jsonStr[startIdx : endIdx+1] if err2 := json.Unmarshal([]byte(jsonBlock), target); err2 != nil { return fmt.Errorf("JSON parse failed: %v (original: %v)", err2, err) } return nil } return fmt.Errorf("JSON parse failed: %v", err) } return nil } // extractParticipantInfoFromText is a fallback parser for participant analysis // when the LLM doesn't return proper JSON. func extractParticipantInfoFromText(content string) ParticipantAnalysisResult { var result ParticipantAnalysisResult lines := strings.Split(content, "\n") var currentParticipant *ParticipantInfo for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } // Try to detect participant sections if strings.Contains(line, "Participant") || strings.Contains(line, "Customer") || strings.Contains(line, "Agent") { if currentParticipant != nil { result.Participants = append(result.Participants, *currentParticipant) } currentParticipant = &ParticipantInfo{} if strings.Contains(line, "Customer") { currentParticipant.Role = "customer" } else if strings.Contains(line, "Agent") { currentParticipant.Role = "agent" } // Try to extract name parts := strings.SplitN(line, ":", 2) if len(parts) > 1 { currentParticipant.Name = strings.TrimSpace(parts[1]) } else { currentParticipant.Name = strings.TrimSpace(strings.ReplaceAll(line, "Participant", "")) } } else if currentParticipant != nil { // Try to extract role, sentiment, etc. from key-value pairs if strings.Contains(line, "Role:") || strings.Contains(line, "role:") { currentParticipant.Role = extractValueAfterColon(line) } else if strings.Contains(line, "Sentiment:") || strings.Contains(line, "sentiment:") { currentParticipant.Sentiment = extractValueAfterColon(line) } else if strings.Contains(line, "Topics:") || strings.Contains(line, "topics:") { topicsStr := extractValueAfterColon(line) topics := strings.Split(topicsStr, ",") for i, t := range topics { topics[i] = strings.TrimSpace(t) } currentParticipant.Topics = topics } } else { // Accumulate as summary text result.Summary += line + " " } } if currentParticipant != nil { result.Participants = append(result.Participants, *currentParticipant) } result.Summary = strings.TrimSpace(result.Summary) // Ensure we always have at least empty arrays if result.Participants == nil { result.Participants = []ParticipantInfo{} } return result } // extractActionItemsFromText is a fallback parser for action items // when the LLM doesn't return proper JSON. func extractActionItemsFromText(content string) ActionItemsResult { var result ActionItemsResult lines := strings.Split(content, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } // Strip numbering/bullet prefixes line = stripListPrefix(line) // Try to detect action item patterns if strings.Contains(line, "TODO") || strings.Contains(line, "Action") || strings.Contains(line, "need to") || strings.Contains(line, "should") || strings.Contains(line, "must") || strings.Contains(line, "follow up") { item := ActionItem{ Description: line, Priority: "medium", Status: "pending", } // Try to extract priority keywords if strings.Contains(line, "urgent") || strings.Contains(line, "immediately") { item.Priority = "high" } else if strings.Contains(line, "low priority") { item.Priority = "low" } result.Items = append(result.Items, item) } } if result.Items == nil { result.Items = []ActionItem{} } return result } // extractLabelSuggestionFromText is a fallback parser for label suggestions // when the LLM doesn't return proper JSON. func extractLabelSuggestionFromText(content string) LabelSuggestionResult { var result LabelSuggestionResult var parsedLabels []string var priority, reason string lines := strings.Split(content, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } if strings.Contains(line, "Label") || strings.Contains(line, "label") || strings.Contains(line, "Tag") || strings.Contains(line, "tag") { value := extractValueAfterColon(line) if value != "" { labels := strings.Split(value, ",") for _, l := range labels { l = strings.TrimSpace(l) if l != "" { parsedLabels = append(parsedLabels, l) } } } } else if strings.Contains(line, "Priority") || strings.Contains(line, "priority") { priority = extractValueAfterColon(line) } else if strings.Contains(line, "Reason") || strings.Contains(line, "reason") { reason = extractValueAfterColon(line) } } if len(parsedLabels) > 0 || priority != "" || reason != "" { result.Suggestions = append(result.Suggestions, ConversationLabelSuggestion{ Labels: parsedLabels, Priority: priority, Reason: reason, }) } return result } // extractValueAfterColon extracts the value portion from a "key: value" line. func extractValueAfterColon(line string) string { parts := strings.SplitN(line, ":", 2) if len(parts) > 1 { return strings.TrimSpace(parts[1]) } return "" } // stripListPrefix removes numbered or bullet list prefixes from a line. func stripListPrefix(line string) string { prefixes := []string{"1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.", "10.", "1)", "2)", "3)", "4)", "5)", "- ", "* ", "• "} for _, prefix := range prefixes { if strings.HasPrefix(line, prefix) { return strings.TrimSpace(strings.TrimPrefix(line, prefix)) } } return line }