diff --git a/pkg/functions/iterative_parser.go b/pkg/functions/iterative_parser.go index 052230e0b..724603993 100644 --- a/pkg/functions/iterative_parser.go +++ b/pkg/functions/iterative_parser.go @@ -305,6 +305,16 @@ func AllSpace(s string) bool { return strings.TrimSpace(s) == "" } +// allSpaceOrEscapedNewlines reports whether s is empty or contains only whitespace +// and the two-character sequences \n and \r (as in escaped JSON or backtick strings). +// Used for XML tool-call prelude checks so that content with literal \n between +// tags is accepted like real newlines, matching behavior when input has actual newlines. +func allSpaceOrEscapedNewlines(s string) bool { + normalized := strings.ReplaceAll(s, "\\n", "") + normalized = strings.ReplaceAll(normalized, "\\r", "") + return strings.TrimSpace(normalized) == "" +} + // TryConsumeJSON attempts to consume a JSON value from the current position // Returns the parsed JSON (can be object, array, or any JSON type), whether it's partial, // and the jsonDumpMarker (non-empty if JSON was healed) @@ -721,7 +731,7 @@ func (p *ChatMsgParser) TryConsumeXMLToolCalls(format *XMLToolCallFormat) (bool, // No more scopes found, break break } - if !AllSpace(tc.Prelude) { + if !allSpaceOrEscapedNewlines(tc.Prelude) { // Non-whitespace before scope_start, stop parsing p.MoveTo(tc.Groups[0].Begin - len(tc.Prelude)) break @@ -743,7 +753,7 @@ func (p *ChatMsgParser) TryConsumeXMLToolCalls(format *XMLToolCallFormat) (bool, break } - if !AllSpace(tc.Prelude) { + if !allSpaceOrEscapedNewlines(tc.Prelude) { // Non-whitespace before tool_start, stop parsing p.MoveTo(tc.Groups[0].Begin - len(tc.Prelude)) break @@ -845,7 +855,7 @@ func (p *ChatMsgParser) TryConsumeXMLToolCalls(format *XMLToolCallFormat) (bool, break } - if !AllSpace(keyStart.Prelude) { + if !allSpaceOrEscapedNewlines(keyStart.Prelude) { // Non-whitespace before key_start, stop parsing parameters p.MoveTo(keyStart.Groups[0].Begin - len(keyStart.Prelude)) break @@ -1009,7 +1019,7 @@ func (p *ChatMsgParser) TryConsumeXMLToolCalls(format *XMLToolCallFormat) (bool, // Rewind to json_end and check if val_end follows p.MoveTo(jsonEnd) valEndSize, valEnd := tryFindValEnd() - if valEnd != nil && AllSpace(valEnd.Prelude) && jsonHealingMarker == "" { + if valEnd != nil && allSpaceOrEscapedNewlines(valEnd.Prelude) && jsonHealingMarker == "" { // val_end follows JSON if len(valEnd.Groups) > 0 { matchedSize := valEnd.Groups[0].End - valEnd.Groups[0].Begin @@ -1105,7 +1115,7 @@ func (p *ChatMsgParser) TryConsumeXMLToolCalls(format *XMLToolCallFormat) (bool, return false, &ChatMsgPartialException{Message: "incomplete tool_call"} } - if !AllSpace(toolEnd.Prelude) { + if !allSpaceOrEscapedNewlines(toolEnd.Prelude) { return returnError(errors.New("non-whitespace before tool_end"), recovery) } @@ -1147,7 +1157,7 @@ func (p *ChatMsgParser) TryConsumeXMLToolCalls(format *XMLToolCallFormat) (bool, break } break - } else if !AllSpace(tc.Prelude) { + } else if !allSpaceOrEscapedNewlines(tc.Prelude) { // Non-whitespace before scope_end - this might be another scope_start // Check if it's actually another scope_start if format.ScopeStart != "" { diff --git a/pkg/functions/parse_test.go b/pkg/functions/parse_test.go index bb68f2ffb..efc0cee91 100644 --- a/pkg/functions/parse_test.go +++ b/pkg/functions/parse_test.go @@ -1726,6 +1726,24 @@ value // Arguments should contain partial flag Expect(results[0].Arguments).To(ContainSubstring("key")) }) + It("should return tool call when leading text precedes tool block (real newlines)", func() { + input := "The memory reclaimer functionality already exists! Let me examine the watchdog to understand how it works and what might need to be implemented for \"auto-fit\" vs unloading.\n\n\n\n\ncd /root/worktrees/LocalAI/task_8562 && cat core/application/watchdog.go\n\n\n" + results, err := ParseXMLIterative(input, nil, true) + Expect(err).NotTo(HaveOccurred()) + Expect(results).NotTo(BeNil()) + Expect(results).To(HaveLen(1)) + Expect(results[0].Name).To(Equal("bash")) + Expect(results[0].Arguments).To(ContainSubstring("task_8562")) + }) + It("should return tool call when leading text precedes tool block (literal \\n between tags)", func() { + input := `The memory reclaimer functionality already exists! Let me examine the watchdog to understand how it works and what might need to be implemented for "auto-fit" vs unloading.\n\n\n\n\ncd /root/worktrees/LocalAI/task_8562 && cat core/application/watchdog.go\n\n\n` + results, err := ParseXMLIterative(input, nil, false) + Expect(err).NotTo(HaveOccurred()) + Expect(results).NotTo(BeNil()) + Expect(results).To(HaveLen(1)) + Expect(results[0].Name).To(Equal("bash")) + Expect(results[0].Arguments).To(ContainSubstring("task_8562")) + }) }) Describe("ParseJSONIterative", func() {