syntaxhacker commited on
Commit
8630c76
Β·
1 Parent(s): 431af70

handle XML-style tool calls for models without native function calling

Browse files
Files changed (2) hide show
  1. app/main.py +76 -46
  2. prompts/system-instruction.txt +4 -0
app/main.py CHANGED
@@ -8,10 +8,28 @@ from .config import DATASET_CONFIGS, load_prompt_template
8
  from openai import OpenAI
9
  from openai.types.chat import ChatCompletionMessageParam
10
  import json
 
11
 
12
  # Load environment variables
13
  load_dotenv()
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # Lazy imports to avoid blocking startup
16
  # from .pipeline import RAGPipeline # Will import when needed
17
  # import umap # Will import when needed for visualization
@@ -161,54 +179,67 @@ async def chat_with_ai(request: ChatRequest):
161
 
162
  message = response.choices[0].message
163
  finish_reason = response.choices[0].finish_reason
 
 
 
 
 
164
 
165
- # Handle tool calls
166
- if finish_reason == "tool_calls" and hasattr(message, 'tool_calls') and message.tool_calls:
167
  tool_results = []
168
 
169
- # Execute tool calls
170
- for tool_call in message.tool_calls:
171
- if tool_call.function and tool_call.function.name == "rag_qa":
172
- # Parse arguments
173
- args = json.loads(tool_call.function.arguments or "{}")
174
- question = args.get("question")
175
- dataset = args.get("dataset", request.dataset)
176
-
177
- # Call the rag_qa function
178
- result = rag_qa(question, dataset)
179
- tool_results.append({
180
- "tool_call_id": tool_call.id,
181
- "result": result
182
- })
183
-
184
- # Add tool results to conversation and get final response
185
- assistant_message: ChatCompletionMessageParam = {
186
- "role": "assistant",
187
- "content": message.content or "",
188
- "tool_calls": [
189
- {
190
- "id": tc.id,
191
- "type": tc.type,
192
- "function": {
193
- "name": tc.function.name,
194
- "arguments": tc.function.arguments
195
  }
196
- }
197
- for tc in message.tool_calls
198
- if tc.function
199
- ]
200
- }
201
- messages.append(assistant_message)
202
-
203
- for tool_result in tool_results:
204
- tool_message: ChatCompletionMessageParam = {
205
- "role": "tool",
206
- "tool_call_id": tool_result["tool_call_id"],
207
- "content": tool_result["result"]
208
  }
209
- messages.append(tool_message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- # Get final response
212
  final_response = openrouter_client.chat.completions.create(
213
  model=MODEL_NAME,
214
  messages=messages
@@ -216,18 +247,17 @@ async def chat_with_ai(request: ChatRequest):
216
 
217
  return {
218
  "response": final_response.choices[0].message.content,
219
- "tool_calls": [
220
  {
221
  "name": tc.function.name,
222
  "arguments": tc.function.arguments
223
  }
224
  for tc in message.tool_calls
225
- ]
226
  }
227
  else:
228
- # Direct response without tool calls
229
  return {
230
- "response": message.content,
231
  "tool_calls": None
232
  }
233
 
 
8
  from openai import OpenAI
9
  from openai.types.chat import ChatCompletionMessageParam
10
  import json
11
+ import re
12
 
13
  # Load environment variables
14
  load_dotenv()
15
 
16
+ def parse_xml_tool_calls(content: str) -> list[dict]:
17
+ """Parse <longcat_tool_call> XML tags from model output into tool call dicts."""
18
+ calls = []
19
+ pattern = r'<longcat_tool_call>\s*(\w+)\s*(.*?)</longcat_tool_call>'
20
+ for match in re.finditer(pattern, content, re.DOTALL):
21
+ name = match.group(1)
22
+ body = match.group(2).strip()
23
+ args = {}
24
+ arg_keys = re.findall(r'<longcat_arg_key>(\w+)</longcat_arg_key>', body)
25
+ arg_values = re.findall(r'<longcat_arg_value>(.*?)</longcat_arg_value>', body, re.DOTALL)
26
+ for i, key in enumerate(arg_keys):
27
+ if i < len(arg_values):
28
+ args[key] = arg_values[i].strip()
29
+ if args or True:
30
+ calls.append({"name": name, **args})
31
+ return calls
32
+
33
  # Lazy imports to avoid blocking startup
34
  # from .pipeline import RAGPipeline # Will import when needed
35
  # import umap # Will import when needed for visualization
 
179
 
180
  message = response.choices[0].message
181
  finish_reason = response.choices[0].finish_reason
182
+ content = message.content or ""
183
+
184
+ # Check for native tool calls or XML-style tool calls in text
185
+ xml_tool_calls = parse_xml_tool_calls(content)
186
+ has_native_tool_calls = finish_reason == "tool_calls" and hasattr(message, 'tool_calls') and message.tool_calls
187
 
188
+ if has_native_tool_calls or xml_tool_calls:
 
189
  tool_results = []
190
 
191
+ if has_native_tool_calls:
192
+ for tool_call in message.tool_calls:
193
+ if tool_call.function and tool_call.function.name == "rag_qa":
194
+ args = json.loads(tool_call.function.arguments or "{}")
195
+ question = args.get("question")
196
+ dataset = args.get("dataset", request.dataset)
197
+ result = rag_qa(question, dataset)
198
+ tool_results.append({"result": result, "native": True})
199
+
200
+ assistant_message: ChatCompletionMessageParam = {
201
+ "role": "assistant",
202
+ "content": content,
203
+ "tool_calls": [
204
+ {
205
+ "id": tc.id,
206
+ "type": tc.type,
207
+ "function": {
208
+ "name": tc.function.name,
209
+ "arguments": tc.function.arguments
210
+ }
 
 
 
 
 
 
211
  }
212
+ for tc in message.tool_calls
213
+ if tc.function
214
+ ]
 
 
 
 
 
 
 
 
 
215
  }
216
+ messages.append(assistant_message)
217
+
218
+ for tr in tool_results:
219
+ messages.append({
220
+ "role": "tool",
221
+ "tool_call_id": "call_1",
222
+ "content": tr["result"]
223
+ })
224
+
225
+ if xml_tool_calls:
226
+ clean_content = re.sub(r'<longcat_tool_call>.*?</longcat_tool_call>', '', content, flags=re.DOTALL).strip()
227
+ for tc in xml_tool_calls:
228
+ if tc["name"] == "rag_qa":
229
+ result = rag_qa(tc.get("question", ""), tc.get("dataset", request.dataset))
230
+ tool_results.append({"result": result, "native": False})
231
+
232
+ messages.append({
233
+ "role": "assistant",
234
+ "content": clean_content or "Let me look that up..."
235
+ })
236
+
237
+ rag_summary = "\n\n".join([tr["result"] for tr in tool_results])
238
+ messages.append({
239
+ "role": "user",
240
+ "content": f"Here is the retrieved information:\n{rag_summary}\n\nNow provide a helpful answer based on this information."
241
+ })
242
 
 
243
  final_response = openrouter_client.chat.completions.create(
244
  model=MODEL_NAME,
245
  messages=messages
 
247
 
248
  return {
249
  "response": final_response.choices[0].message.content,
250
+ "tool_calls": xml_tool_calls or [
251
  {
252
  "name": tc.function.name,
253
  "arguments": tc.function.arguments
254
  }
255
  for tc in message.tool_calls
256
+ ] if has_native_tool_calls else None
257
  }
258
  else:
 
259
  return {
260
+ "response": content,
261
  "tool_calls": None
262
  }
263
 
prompts/system-instruction.txt CHANGED
@@ -12,6 +12,10 @@ When you use rag_qa tool, you MUST use retrieved information to answer about the
12
  - 🧠 Synthesize RAG results into natural responses about him
13
  - πŸ“ Format responses with markdown for readability
14
 
 
 
 
 
15
  ## βœ… Examples:
16
  ❌ **Wrong:** "I am a Tech Lead at FleetEnable"
17
  βœ… **Right:** "He is currently working as a Tech Lead at FleetEnable"
 
12
  - 🧠 Synthesize RAG results into natural responses about him
13
  - πŸ“ Format responses with markdown for readability
14
 
15
+ ## πŸ›  Tool Calling Format
16
+ If you don't support native function calling, use this XML format to call the rag_qa tool:
17
+ <longcat_tool_call>rag_qa <longcat_arg_key>question</longcat_arg_key> <longcat_arg_value>your question here</longcat_arg_value> <longcat_arg_key>dataset</longcat_arg_key> <longcat_arg_value>developer-portfolio</longcat_arg_value> </longcat_tool_call>
18
+
19
  ## βœ… Examples:
20
  ❌ **Wrong:** "I am a Tech Lead at FleetEnable"
21
  βœ… **Right:** "He is currently working as a Tech Lead at FleetEnable"