From 041f2337dfe1f8dd9e22b1bc3b5237a7c9a8dcc8 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:05:43 +0000 Subject: [PATCH] fix(chat): preserve complete tool source URLs Keep balanced parentheses in source URLs and decode MCP text arrays before extracting links. This preserves Wikipedia paths and escaped query separators instead of appending truncated citations. Assisted-by: Codex:gpt-6 Signed-off-by: Stefan Walcz (cherry picked from commit 9b81cb20ab956357f8151378905c7de2d9f6708f) --- core/http/endpoints/openai/evidence.go | 19 ++++++++++++++++--- .../endpoints/openai/evidence_sources_test.go | 14 ++++++++++++++ docs/content/features/model-aliases.md | 3 ++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/http/endpoints/openai/evidence.go b/core/http/endpoints/openai/evidence.go index 5f7c782e10..2fd9ecd870 100644 --- a/core/http/endpoints/openai/evidence.go +++ b/core/http/endpoints/openai/evidence.go @@ -1,6 +1,7 @@ package openai import ( + "encoding/json" "regexp" "strings" @@ -12,9 +13,9 @@ import ( var numberToken = regexp.MustCompile(`\b\d+(?:[.,]\d+)?\b`) // urlToken matches an http(s) URL up to the first character that cannot be -// part of one in tool output: whitespace, a quote, a closing bracket of -// Markdown or JSON, or the backslash of a JSON escape. -var urlToken = regexp.MustCompile(`https?://[^\s"'<>()\[\]{}\\]+`) +// part of one in tool output. Parentheses are allowed inside URL paths; +// sourceURLs removes unmatched closing parentheses from Markdown links. +var urlToken = regexp.MustCompile(`https?://[^\s"'<>\[\]{}\\]+`) // maxSourceURLs caps the appended source line; a search tool can return // dozens of links, and the first ones are the ones it ranked highest. @@ -27,8 +28,20 @@ func sourceURLs(texts []string) []string { var urls []string seen := map[string]struct{}{} for _, text := range texts { + // ExecuteMCPToolCall JSON-encodes multiple TextContent results. + // Decode that envelope so escaped query separators remain intact. + var parts []string + var single string + if json.Unmarshal([]byte(text), &parts) == nil { + text = strings.Join(parts, "\n") + } else if json.Unmarshal([]byte(text), &single) == nil { + text = single + } for _, token := range urlToken.FindAllString(text, -1) { token = strings.TrimRight(token, ".,;:!?") + for strings.HasSuffix(token, ")") && strings.Count(token, ")") > strings.Count(token, "(") { + token = strings.TrimRight(strings.TrimSuffix(token, ")"), ".,;:!?") + } if _, ok := seen[token]; ok || strings.HasSuffix(token, "://") { continue } diff --git a/core/http/endpoints/openai/evidence_sources_test.go b/core/http/endpoints/openai/evidence_sources_test.go index 9b66178422..6921c9f0df 100644 --- a/core/http/endpoints/openai/evidence_sources_test.go +++ b/core/http/endpoints/openai/evidence_sources_test.go @@ -1,11 +1,25 @@ package openai import ( + "encoding/json" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("sourceURLs", func() { + It("preserves parentheses inside URLs and removes surrounding Markdown punctuation", func() { + url := "https://en.wikipedia.org/wiki/Mercury_(planet)" + Expect(sourceURLs([]string{"[Mercury](" + url + "). (" + url + ")"})).To(Equal([]string{url})) + }) + + It("decodes multiple MCP text results before extracting URLs", func() { + url := "https://example.com/search?a=1&b=2" + encoded, err := json.Marshal([]string{"First " + url + "\n", "Second https://example.com/other"}) + Expect(err).NotTo(HaveOccurred()) + Expect(sourceURLs([]string{string(encoded)})).To(Equal([]string{url, "https://example.com/other"})) + }) + It("returns unique URLs in order of appearance, at most five", func() { text := "x https://1.example/a, y (https://2.example/b) z https://1.example/a " + "[l](https://3.example/c). \"https://4.example/d\" http://5.example/e; https://6.example/f" diff --git a/docs/content/features/model-aliases.md b/docs/content/features/model-aliases.md index 0363f26b8d..bcf343c7ee 100644 --- a/docs/content/features/model-aliases.md +++ b/docs/content/features/model-aliases.md @@ -160,7 +160,8 @@ deterministic and applies to each request on its own: 3. Otherwise the distinct URLs of those tool results are collected in the order they first appear, at most five, and appended after a blank line as one line: `Quellen: https://a.example/test, https://b.example/oil`. Punctuation that - ends a sentence is not taken as part of a URL. + ends a sentence is not taken as part of a URL. Parentheses inside URL paths + and query parameters in JSON-encoded MCP text results are preserved. 4. If no tool result contains a URL, nothing is appended. No URL is ever made up: every link on the line was returned by a tool.