Files
gochat/internal/service/llm_response_parser.go
T
2026-06-04 15:44:48 +08:00

214 lines
6.7 KiB
Go

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
}