11package org .beehive .gpullama3 .model .format ;
22
3+ import java .util .ArrayList ;
4+ import java .util .List ;
35import java .util .Optional ;
46
57/**
@@ -41,6 +43,11 @@ public static Optional<ToolCallExtract> parseLlamaResponse(String responseText)
4143 String json = responseText .substring (tcStart + "<tool_call>" .length (), tcEnd ).strip ();
4244 return parseLlamaJson (json );
4345 }
46+ // 2b. Unclosed <tool_call> — model stopped (eot_id / eom_id) before writing the closing tag
47+ if (tcStart != -1 && tcEnd == -1 ) {
48+ String json = responseText .substring (tcStart + "<tool_call>" .length ()).strip ();
49+ return parseLlamaJson (json );
50+ }
4451
4552 // 3. Fallback: raw JSON, possibly inside markdown code fences
4653 String stripped = stripMarkdownFences (responseText .strip ());
@@ -72,16 +79,66 @@ private static Optional<ToolCallExtract> parseLlamaJson(String json) {
7279
7380 // ── Qwen3 ─────────────────────────────────────────────────────────────────
7481
82+ /**
83+ * Extracts ALL tool calls from a response that may contain multiple
84+ * {@code <tool_call>…</tool_call>} blocks (Llama 3.2 and Qwen3 batch calls).
85+ *
86+ * Falls back to the raw-JSON single-call path if no tags are found.
87+ * Returns an empty list when the response contains no tool calls.
88+ */
89+ public static List <ToolCallExtract > parseAllToolCalls (String responseText ) {
90+ List <ToolCallExtract > calls = new java .util .ArrayList <>();
91+
92+ // <|python_tag|> (Llama 3.1) — single call by definition
93+ int pythonIdx = responseText .indexOf ("<|python_tag|>" );
94+ if (pythonIdx != -1 ) {
95+ parseLlamaJson (responseText .substring (pythonIdx + "<|python_tag|>" .length ()).strip ())
96+ .ifPresent (calls ::add );
97+ return calls ;
98+ }
99+
100+ // Scan for all <tool_call>…</tool_call> blocks
101+ int searchFrom = 0 ;
102+ while (true ) {
103+ int start = responseText .indexOf ("<tool_call>" , searchFrom );
104+ if (start == -1 ) break ;
105+ int end = responseText .indexOf ("</tool_call>" , start );
106+ String json ;
107+ if (end != -1 ) {
108+ json = responseText .substring (start + "<tool_call>" .length (), end ).strip ();
109+ searchFrom = end + "</tool_call>" .length ();
110+ } else {
111+ // Unclosed tag — model stopped before writing the closing tag
112+ json = responseText .substring (start + "<tool_call>" .length ()).strip ();
113+ searchFrom = responseText .length ();
114+ }
115+ parseLlamaJson (json ).ifPresent (calls ::add );
116+ if (end == -1 ) break ;
117+ }
118+
119+ // Raw JSON fallback (no tags at all)
120+ if (calls .isEmpty ()) {
121+ String stripped = stripMarkdownFences (responseText .strip ());
122+ if (stripped .startsWith ("{" )) {
123+ parseLlamaJson (stripped ).ifPresent (calls ::add );
124+ }
125+ }
126+
127+ return calls ;
128+ }
129+
75130 /**
76131 * Extracts a tool call enclosed in {@code <tool_call>…</tool_call>} tags
77132 * as produced by Qwen3 models.
78133 */
79134 public static Optional <ToolCallExtract > parseQwen3Response (String responseText ) {
80135 int start = responseText .indexOf ("<tool_call>" );
81136 int end = responseText .lastIndexOf ("</tool_call>" );
82- if (start == -1 || end == - 1 || end <= start ) return Optional .empty ();
137+ if (start == -1 ) return Optional .empty ();
83138
84- String json = responseText .substring (start + "<tool_call>" .length (), end ).strip ();
139+ String json = (end != -1 && end > start )
140+ ? responseText .substring (start + "<tool_call>" .length (), end ).strip ()
141+ : responseText .substring (start + "<tool_call>" .length ()).strip ();
85142
86143 String name = extractStringValue (json , "name" );
87144 if (name == null ) return Optional .empty ();
@@ -104,7 +161,11 @@ public static String stripMarkdownFences(String text) {
104161 return body .strip ();
105162 }
106163
107- /** Extracts the string value for {@code "key": "<value>"} from a JSON object. Tolerates whitespace around {@code :}. */
164+ /**
165+ * Extracts the string value for {@code "key": "<value>"} from a JSON object.
166+ * Tolerates whitespace around {@code :} and correctly skips escaped quotes ({@code \"})
167+ * inside the value, so multi-line code strings with embedded {@code "} are returned intact.
168+ */
108169 public static String extractStringValue (String json , String key ) {
109170 String marker = "\" " + key + "\" " ;
110171 int markerIdx = json .indexOf (marker );
@@ -113,9 +174,20 @@ public static String extractStringValue(String json, String key) {
113174 if (colonIdx == -1 ) return null ;
114175 int quoteStart = json .indexOf ('"' , colonIdx + 1 );
115176 if (quoteStart == -1 ) return null ;
116- int quoteEnd = json .indexOf ('"' , quoteStart + 1 );
117- if (quoteEnd == -1 ) return null ;
118- return json .substring (quoteStart + 1 , quoteEnd );
177+ // Scan for the closing quote, honouring backslash escapes
178+ int i = quoteStart + 1 ;
179+ while (i < json .length ()) {
180+ char c = json .charAt (i );
181+ if (c == '\\' ) {
182+ i += 2 ; // skip escape sequence (e.g. \", \\, \n)
183+ } else if (c == '"' ) {
184+ break ;
185+ } else {
186+ i ++;
187+ }
188+ }
189+ if (i >= json .length ()) return null ;
190+ return json .substring (quoteStart + 1 , i );
119191 }
120192
121193 /**
0 commit comments