package ai import ( "encoding/json" "fmt" "strings" ) // parseSuggestions extracts []Suggestion from an LLM text reply, tolerating // ```json fences and surrounding prose. Priorities are clamped to 0..2 and // the result is capped to a sane number of items. func parseSuggestions(reply string) ([]Suggestion, error) { text := stripFences(strings.TrimSpace(reply)) start := strings.IndexByte(text, '[') end := strings.LastIndexByte(text, ']') if start == -1 || end == -1 || end <= start { return nil, fmt.Errorf("ai: no JSON array in reply: %s", truncate(reply, 200)) } var out []Suggestion if err := json.Unmarshal([]byte(text[start:end+1]), &out); err != nil { return nil, fmt.Errorf("ai: parse suggestions: %w", err) } if len(out) > 50 { out = out[:50] } for i := range out { if out[i].Priority < 0 { out[i].Priority = 0 } if out[i].Priority > 2 { out[i].Priority = 2 } } return out, nil } // stripFences removes ```...``` code fences so fenced JSON parses cleanly. func stripFences(s string) string { if !strings.Contains(s, "```") { return s } lines := strings.Split(s, "\n") out := make([]string, 0, len(lines)) inFence := false for _, line := range lines { if strings.HasPrefix(strings.TrimSpace(line), "```") { inFence = !inFence continue } if inFence { out = append(out, line) } } if len(out) == 0 { return s // fences never closed; fall back to raw content } return strings.Join(out, "\n") } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." }