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 <stefan.walcz@walcz.de>
(cherry picked from commit 9b81cb20ab)
This commit is contained in:
localai-org-maint-bot authored and Ettore Di Giacinto committed 2026-11-03 19:18:35 +00:00
1 parent 23012a7c4a
commit 041f2337df
3 files changed
+32 -4

No files matched your search

+16 -3
View File
@@ -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
}
@@ -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"
+2 -1
View File
@@ -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.