Commit 63bed9c0 authored by Your Name's avatar Your Name

Add ToolCallParser as fallback for all model parsers and enhance multi-line tool call parsing

- Added _parse_multiline_tool_calls() method to handle multi-line tool_call format
- Added ToolCallParser fallback to all specific model parsers:
  - QwenParser, DeepSeekParser, LlamaParser, MistralParser
  - ClaudeParser, CommandRParser, GemmaParser, GrokParser
  - PhiParser, ApexBig50Parser
- Updated extract_tool_calls() to use both XML and multi-line parsing
- Tested and verified tool call extraction works for user's example format
parent 43873a46
......@@ -235,6 +235,14 @@ class QwenParser(BaseParser):
if not results:
results = self._parse_coder_style(clean_text)
# 5. Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG QwenParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
def _parse_coder_style(self, text: str):
......@@ -287,6 +295,14 @@ class DeepSeekParser(BaseParser):
except:
continue
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG DeepSeekParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -305,6 +321,14 @@ class LlamaParser(BaseParser):
except:
continue
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG LlamaParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -323,6 +347,14 @@ class MistralParser(BaseParser):
except:
pass
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG MistralParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -343,6 +375,14 @@ class ClaudeParser(BaseParser):
args = args_match.group(1) if args_match else {}
results.append(self._to_oa(name_match.group(1), args))
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG ClaudeParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -360,6 +400,14 @@ class CommandRParser(BaseParser):
except:
pass
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG CommandRParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -377,6 +425,14 @@ class GemmaParser(BaseParser):
except:
pass
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG GemmaParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -394,6 +450,14 @@ class GrokParser(BaseParser):
except:
pass
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG GrokParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -416,6 +480,14 @@ class PhiParser(BaseParser):
if not results:
results = LlamaParser(self.tools).parse(text)
# Fallback: if still no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG PhiParser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -473,6 +545,14 @@ class ApexBig50Parser(BaseParser):
except:
pass
# Fallback: if no tool calls found, try using ToolCallParser
if not results:
tool_call_parser = ToolCallParser()
fallback_calls = tool_call_parser.extract_tool_calls(text, [])
if fallback_calls:
print(f"DEBUG ApexBig50Parser: ToolCallParser fallback found {len(fallback_calls)} tool calls")
results.extend(fallback_calls)
return results
......@@ -1226,6 +1306,111 @@ class ToolCallParser:
return unique_tool_calls
def _parse_multiline_tool_calls(self, text: str) -> List[Dict]:
"""Parse multi-line tool_call format with newlines between tags.
This handles format like:
<tool_call>
<tool>
<name>search</name>
<arguments>
{"query": "Apple AAPL Q4 2023"}
</arguments>
</tool>
<tool>
<name>search</name>
<arguments>
{"query": "Microsoft MSFT Q4 2023"}
</arguments>
</tool>
</tool_call>
"""
tool_calls = []
# Pattern for multi-line tool_call with separate tool blocks
# This pattern is more lenient with whitespace and newlines
pattern_multiline = r'<tool_call>\s*(.*?)\s*</tool_call>'
matches = re.findall(pattern_multiline, text, re.DOTALL | re.IGNORECASE)
for match in matches:
# Find each <tool> block within the tool_call
tool_blocks = re.findall(r'<tool>\s*(.*?)\s*</tool>', match, re.DOTALL | re.IGNORECASE)
for tool_block in tool_blocks:
# Extract name
name_match = re.search(r'<name>\s*(.*?)\s*</name>', tool_block, re.DOTALL | re.IGNORECASE)
if not name_match:
continue
name = name_match.group(1).strip()
if not name:
continue
# Extract arguments - could be JSON or XML-style
args_match = re.search(r'<arguments>\s*(.*?)\s*</arguments>', tool_block, re.DOTALL | re.IGNORECASE)
if not args_match:
# Try <parameters> as alternative
args_match = re.search(r'<parameters>\s*(.*?)\s*</parameters>', tool_block, re.DOTALL | re.IGNORECASE)
if args_match:
args_str = args_match.group(1).strip()
try:
args = json.loads(args_str) if args_str else {}
except json.JSONDecodeError:
# Try cleaning up the JSON string
cleaned = args_str.replace('\n', ' ').replace('\r', '')
try:
args = json.loads(cleaned)
except:
# Try extracting key-value pairs from XML
args = {}
for k, v in re.findall(r'<(\w+)>\s*(.*?)\s*</\1>', args_str, re.DOTALL):
args[k] = v.strip()
else:
args = {}
tool_calls.append({
"id": f"call_{uuid.uuid4().hex[:16]}",
"type": "function",
"function": {
"name": name,
"arguments": json.dumps(args)
}
})
# Also try simpler pattern for standalone multi-line tools
# <tool>
# <name>search</name>
# <arguments>{...}</arguments>
# </tool>
standalone_pattern = r'<tool>\s*<name>\s*(.*?)\s*</name>\s*<arguments>\s*(.*?)\s*</arguments>\s*</tool>'
standalone_matches = re.findall(standalone_pattern, text, re.DOTALL | re.IGNORECASE)
for name, args_str in standalone_matches:
name = name.strip()
if not name:
continue
try:
args = json.loads(args_str.strip()) if args_str.strip() else {}
except json.JSONDecodeError:
cleaned = args_str.replace('\n', ' ').replace('\r', '')
try:
args = json.loads(cleaned)
except:
args = {}
tool_calls.append({
"id": f"call_{uuid.uuid4().hex[:16]}",
"type": "function",
"function": {
"name": name,
"arguments": json.dumps(args)
}
})
return tool_calls
def set_model_name(self, model_name: str):
"""Set the model name for model-specific parsing."""
self.model_name = model_name
......@@ -1314,6 +1499,13 @@ class ToolCallParser:
print(f"DEBUG ToolCallParser: XML-style parsing found {len(xml_tool_calls)} tool calls")
tool_calls.extend(xml_tool_calls)
# If still no tool calls, try multi-line format parsing
if not tool_calls:
multiline_tool_calls = self._parse_multiline_tool_calls(text)
if multiline_tool_calls:
print(f"DEBUG ToolCallParser: Multi-line parsing found {len(multiline_tool_calls)} tool calls")
tool_calls.extend(multiline_tool_calls)
# Debug output for results
if tool_calls:
print(f"DEBUG ToolCallParser: Returning {len(tool_calls)} tool calls")
......
......@@ -2335,13 +2335,22 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request
**extra_params,
):
reasoning_text += chunk
# Debug: log first pass chunks
if global_debug:
print(f"DEBUG FIRST PASS: chunk length={len(chunk)}, total reasoning so far={len(reasoning_text)}")
yield f"data: {json.dumps({'choices': [{'delta': {'content': chunk}, 'finish_reason': None}]})}\n\n"
# Check if we hit the close tag
if close_tag and close_tag in reasoning_text:
if global_debug:
print(f"DEBUG: Close tag detected in first pass, reasoning length={len(reasoning_text)}")
break
else:
# Fallback: non-streaming
if global_debug:
print(f"DEBUG: Using non-streaming fallback for first pass")
first_pass_result = current_manager.generate(
prompt=raw_prompt_for_generation,
max_tokens=request.max_tokens or 2048,
......@@ -2397,8 +2406,13 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request
if not text_for_tool_extraction or not text_for_tool_extraction.strip():
if global_debug:
print(f"DEBUG: Second pass result is empty, trying reasoning text")
print(f"DEBUG: Reasoning text length: {len(reasoning_text)}")
print(f"DEBUG: Reasoning text preview: {reasoning_text[:200] if reasoning_text else 'empty'}")
text_for_tool_extraction = reasoning_text
if global_debug:
print(f"DEBUG: Final text for tool extraction: {text_for_tool_extraction[:200] if text_for_tool_extraction else 'empty'}")
if request.tools and text_for_tool_extraction:
# Convert tools for ModelParserAdapter
from codai.pydantic.textrequest import Tool, ToolFunction
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment