package agents import ( "fmt" "net/url" "strings" "sync" ) type kbCitationList struct { mu sync.Mutex citations []KBCitation } func (l *kbCitationList) AddKBCitations(citations []KBCitation) { if len(citations) == 0 { return } l.mu.Lock() defer l.mu.Unlock() l.citations = append(l.citations, citations...) } func (l *kbCitationList) Citations() []KBCitation { l.mu.Lock() defer l.mu.Unlock() out := make([]KBCitation, len(l.citations)) copy(out, l.citations) return out } // AppendKBCitations appends a markdown Sources block for KB citations. func AppendKBCitations(response, collection, userID string, citations []KBCitation) string { if strings.TrimSpace(response) == "" || len(citations) == 0 { return response } var lines []string seen := make(map[string]struct{}) for _, citation := range citations { key := strings.TrimSpace(citation.EntryKey) if key == "" { key = strings.TrimSpace(citation.FileName) } if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} displayName := kbCitationDisplayName(citation) if displayName == "" { continue } sourceURL := kbCitationRawFileURL(collection, citation.EntryKey, userID) number := len(lines) + 1 if sourceURL == "" { lines = append(lines, fmt.Sprintf("[%d] %s", number, displayName)) continue } lines = append(lines, fmt.Sprintf("[%d] [%s](%s)", number, escapeMarkdownLinkText(displayName), sourceURL)) } if len(lines) == 0 { return response } var sb strings.Builder sb.WriteString(strings.TrimRight(response, "\n")) sb.WriteString("\n\nSources:\n") for _, line := range lines { sb.WriteString(line) sb.WriteString("\n") } return strings.TrimRight(sb.String(), "\n") } func kbCitationDisplayName(citation KBCitation) string { if fileName := strings.TrimSpace(citation.FileName); fileName != "" { return fileName } segments := strings.Split(strings.Trim(strings.TrimSpace(citation.EntryKey), "/"), "/") for i := len(segments) - 1; i >= 0; i-- { if segment := strings.TrimSpace(segments[i]); segment != "" { return segment } } return "" } func kbCitationRawFileURL(collection, entryKey, userID string) string { collection = strings.TrimSpace(collection) entryKey = strings.Trim(strings.TrimSpace(entryKey), "/") if collection == "" || entryKey == "" { return "" } var escapedEntrySegments []string for _, segment := range strings.Split(entryKey, "/") { if segment == "" { continue } escapedEntrySegments = append(escapedEntrySegments, url.PathEscape(segment)) } if len(escapedEntrySegments) == 0 { return "" } sourceURL := "/api/agents/collections/" + url.PathEscape(collection) + "/entries-raw/" + strings.Join(escapedEntrySegments, "/") if userID != "" { query := url.Values{} query.Set("user_id", userID) sourceURL += "?" + query.Encode() } return sourceURL } func escapeMarkdownLinkText(text string) string { text = strings.ReplaceAll(text, `\`, `\\`) text = strings.ReplaceAll(text, "[", `\[`) text = strings.ReplaceAll(text, "]", `\]`) return text }