Commit 82ee7353 authored by Your Name's avatar Your Name

Integrate model_parser module as codai package

- Move model_parser.py into codai/ directory
- Add __init__.py to make it a proper Python module
- Create ModelParserAdapter class to wrap ModelParserDispatcher
- Replace ToolCallParser() with ModelParserAdapter() in 4 locations
- Update import to use 'from codai import ModelParserDispatcher'

This enables model-specific tool call parsing for Qwen, DeepSeek,
Llama, Mistral, Claude, Command R, Gemma, Grok, and Phi models.
parent fbb6476e
# codai module - AI model parsing utilities
from .model_parser import (
ModelParserDispatcher,
BaseParser,
QwenParser,
DeepSeekParser,
LlamaParser,
MistralParser,
ClaudeParser,
CommandRParser,
GemmaParser,
GrokParser,
PhiParser,
ApexBig50Parser,
)
__all__ = [
'ModelParserDispatcher',
'BaseParser',
'QwenParser',
'DeepSeekParser',
'LlamaParser',
'MistralParser',
'ClaudeParser',
'CommandRParser',
'GemmaParser',
'GrokParser',
'PhiParser',
'ApexBig50Parser',
]
"""
Model Parser Dispatcher - Multi-Model Tool Call Parsing
This module provides a comprehensive tool call parsing system that supports
multiple model types with different parsing strategies and includes validation.
Usage:
# Initialize dispatcher
dispatcher = ModelParserDispatcher(model_name="qwen3-7b-instruct", tools_schema=my_tools)
# Parse tool calls
raw_reply = '<tool_call>{"name": "get_weather", "arguments": {"location": "London"}}</tool_call>'
standard_output = dispatcher.parse(raw_reply)
"""
import functools
import json
import uuid
import re
from difflib import get_close_matches
from typing import Dict, List, Any, Optional
def validate_tool_output(func):
"""Decorator to validate and standardize tool call output."""
@functools.wraps(func)
def wrapper(self, text, *args, **kwargs):
raw_calls = func(self, text, *args, **kwargs)
if not raw_calls or not self.tools:
return raw_calls
validated = []
tool_names = list(self.tools.keys())
for call in raw_calls:
name = call['function']['name']
# Fuzzy Match Name
if name not in self.tools:
match = get_close_matches(name, tool_names, n=1, cutoff=0.7)
name = match[0] if match else name
# Ensure Arguments are a clean JSON string
try:
args_dict = json.loads(call['function']['arguments']) if isinstance(call['function']['arguments'], str) else call['function']['arguments']
except:
args_dict = {}
# Type Casting based on Schema
if name in self.tools:
props = self.tools[name].get('parameters', {}).get('properties', {})
for k, v in list(args_dict.items()):
if k in props:
t = props[k].get('type')
if t == "integer":
args_dict[k] = int(v) if str(v).isdigit() else v
if t == "boolean":
args_dict[k] = str(v).lower() in ["true", "1", "yes"]
validated.append({
"id": f"call_{uuid.uuid4().hex[:12]}",
"type": "function",
"function": {"name": name, "arguments": json.dumps(args_dict)}
})
return validated
return wrapper
class BaseParser:
"""Base parser class for tool calls."""
def __init__(self, tools: Dict[str, Any] = None):
self.tools = tools or {}
def _to_oa(self, name: str, args: Any) -> Dict:
"""Convert to OpenAI format."""
if args is None:
args = {}
if isinstance(args, str):
try:
args = json.loads(args)
except:
args = {}
return {"name": name, "arguments": args}
# 1. QWEN PARSER (Instruct & Coder Style)
class QwenParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
# Clean text first
clean_text = re.sub(r'<\|.*?\|>', '', text)
# Use raw string for regex with special tokens
think_pattern = r'<think>.*?</think>'
clean_text = re.sub(think_pattern, '', clean_text, flags=re.DOTALL)
# INSTRUCT STYLE: <tool_call>{"name": "...", "arguments": {...}}</tool_call>
instruct_matches = re.findall(r'<tool_call>\s*(\{.*?\})\s*</tool_call>', clean_text, re.DOTALL)
for match in instruct_matches:
try:
data = json.loads(match.strip())
if 'name' in data and 'arguments' in data:
results.append(self._to_oa(data['name'], data['arguments']))
except:
continue
# CODER STYLE: <tool_call><function=name><parameter=key>value</parameter></function></tool_call>
if not results:
coder_blocks = re.findall(r'<tool_call>\s*(.*?)\s*</tool_call>', clean_text, re.DOTALL)
if not coder_blocks:
coder_blocks = re.findall(r'(<function=.*?</function>)', clean_text, re.DOTALL)
for block in coder_blocks:
func_name_match = re.search(r'<function=([^>]+)>', block)
if func_name_match:
func_name = func_name_match.group(1).strip()
params = re.findall(r'<parameter=([^>]+)>(.*?)</parameter>', block, re.DOTALL)
arguments = {}
for k, v in params:
key = k.strip()
val = v.strip()
try:
arguments[key] = json.loads(val)
except:
arguments[key] = val
results.append(self._to_oa(func_name, arguments))
return results
# 2. DEEPSEEK PARSER
class DeepSeekParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
# DeepSeek-V3 uses specialized JSON prompts
calls = re.findall(r'\{"name":\s*"(.*?)",\s*"parameters":\s*(\{.*?\})}', text)
for name, params in calls:
try:
results.append(self._to_oa(name, json.loads(params)))
except:
continue
return results
# 3. LLAMA PARSER (Markdown JSON)
class LlamaParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
blocks = re.findall(r'```json\s*([\[\{].*?[\]\}])\s*```', text, re.DOTALL)
for block in blocks:
try:
data = json.loads(block)
for item in (data if isinstance(data, list) else [data]):
results.append(self._to_oa(item.get("name"), item.get("arguments")))
except:
continue
return results
# 4. MISTRAL PARSER
class MistralParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
match = re.search(r'\[TOOL_CALLS\]\s*(.*)', text)
if match:
try:
calls = json.loads(match.group(1))
for c in calls:
results.append(self._to_oa(c["name"], c["arguments"]))
except:
pass
return results
# 5. CLAUDE PARSER
class ClaudeParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
calls = re.findall(r'<tool_use>(.*?)</tool_use>', text, re.DOTALL)
for c in calls:
name_match = re.search(r'<name>(.*?)</name>', c)
args_match = re.search(r'<parameters>(.*?)</parameters>', c, re.DOTALL)
if name_match:
try:
args = json.loads(args_match.group(1)) if args_match else {}
except:
args = args_match.group(1) if args_match else {}
results.append(self._to_oa(name_match.group(1), args))
return results
# 6. COMMAND R PARSER
class CommandRParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
action = re.search(r'Action:\s*(.*)', text)
args = re.search(r'Action Input:\s*(\{.*\})', text)
if action and args:
try:
results.append(self._to_oa(action.group(1).strip(), json.loads(args.group(1))))
except:
pass
return results
# 7. GEMMA PARSER
class GemmaParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
match = re.search(r'{\s*"name":\s*".*?"\s*,\s*"parameters":\s*\{.*?\}\s*\}', text, re.DOTALL)
if match:
try:
data = json.loads(match.group(0))
results.append(self._to_oa(data["name"], data["parameters"]))
except:
pass
return results
# 8. GROK PARSER
class GrokParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
try:
data = json.loads(text)
if isinstance(data, list) and len(data) > 0 and "name" in data[0]:
for c in data:
results.append(self._to_oa(c["name"], c["arguments"]))
except:
pass
return results
# 9. PHI PARSER
class PhiParser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
# Phi-3/4 uses <|tool_call|> tags
match = re.search(r'<\|tool_call\|>(.*?)<\|', text, re.DOTALL)
if match:
try:
data = json.loads(match.group(1))
results.append(self._to_oa(data["name"], data["arguments"]))
except:
pass
# Fallback to Llama-style markdown
if not results:
results = LlamaParser(self.tools).parse(text)
return results
# 10. APEX BIG 50 (Catch-All Parser)
class ApexBig50Parser(BaseParser):
@validate_tool_output
def parse(self, text: str) -> List[Dict]:
results = []
# XML patterns
xml_patterns = [
r'<(?:tool_call|function_call|tool_use)>(.*?)</(?:tool_call|function_call|tool_use)>',
r'\[TOOL_CALLS\](.*?)\[/TOOL_CALLS\]'
]
for p in xml_patterns:
for match in re.findall(p, text, re.DOTALL):
try:
data = json.loads(match.strip())
items = data if isinstance(data, list) else [data]
for i in items:
name = i.get("name") or i.get("function")
args = i.get("arguments") or i.get("args")
if name:
results.append(self._to_oa(name, args))
except:
fn = re.search(r'<(?:function|name)=(.*?)>', match)
if fn:
params = dict(re.findall(r'<(?:parameter|arg|argument)=(.*?)>(.*?)</(?:parameter|arg|argument)>', match, re.DOTALL))
results.append(self._to_oa(fn.group(1).strip(), params))
# Markdown patterns
md_patterns = [
r'```json\s*([\[\{].*?[\]\}])\s*```',
]
for p in md_patterns:
for block in re.findall(p, text, re.DOTALL):
try:
data = json.loads(block)
for item in (data if isinstance(data, list) else [data]):
name = item.get("name") or item.get("function")
args = item.get("arguments") or item.get("parameters") or item
if name and isinstance(name, str):
results.append(self._to_oa(name, args))
except:
pass
# React pattern
react_matches = re.findall(r'Action:\s*(.*?)\nAction Input:\s*(\{.*?\})', text, re.DOTALL)
for name, args_raw in react_matches:
try:
results.append(self._to_oa(name.strip(), json.loads(args_raw.strip())))
except:
pass
return results
# Model Parser Dispatcher
class ModelParserDispatcher:
"""Dispatcher to select the appropriate parser based on model name."""
def __init__(self, model_name: str = None, tools_schema: Dict[str, Any] = None):
self.model_name = model_name
self.tools = tools_schema or {}
self.parser = self._get_parser()
def _get_parser(self) -> BaseParser:
"""Get the appropriate parser based on model name."""
if not self.model_name:
return ApexBig50Parser(self.tools)
model_lower = self.model_name.lower()
# Qwen models
if 'qwen' in model_lower:
return QwenParser(self.tools)
# DeepSeek models
if 'deepseek' in model_lower:
return DeepSeekParser(self.tools)
# Llama models
if 'llama' in model_lower:
return LlamaParser(self.tools)
# Mistral models
if 'mistral' in model_lower or 'mixtral' in model_lower:
return MistralParser(self.tools)
# Claude models
if 'claude' in model_lower:
return ClaudeParser(self.tools)
# Command R models
if 'command' in model_lower:
return CommandRParser(self.tools)
# Gemma models
if 'gemma' in model_lower:
return GemmaParser(self.tools)
# Grok models
if 'grok' in model_lower:
return GrokParser(self.tools)
# Phi models
if 'phi' in model_lower:
return PhiParser(self.tools)
# Default: use catch-all parser
return ApexBig50Parser(self.tools)
def parse(self, text: str) -> List[Dict]:
"""Parse tool calls from model output."""
return self.parser.parse(text)
def set_tools(self, tools: Dict[str, Any]) -> None:
"""Update the tools schema."""
self.tools = tools
self.parser.tools = tools
......@@ -27,6 +27,9 @@ from fastapi.responses import StreamingResponse, FileResponse
from pydantic import BaseModel, Field, validator, field_validator, ConfigDict
from pydantic_core import PydanticCustomError
from threading import Thread
# Import codai module for enhanced tool call parsing
from codai import ModelParserDispatcher
# Per-model semaphores for request concurrency control
model_semaphores: dict = {}
load_mode = {"mode": "ondemand"} # Track load mode globally
......@@ -756,6 +759,94 @@ class ToolCallParser:
# Don't strip whitespace - spaces and newlines are valid content
return text
# =============================================================================
# Model Parser Dispatcher Wrapper - integrates model_parser module
# =============================================================================
class ModelParserAdapter:
"""Adapter class that wraps ModelParserDispatcher to provide ToolCallParser interface.
This allows seamless integration of the model_parser module while maintaining
compatibility with the existing coderai codebase.
"""
def __init__(self, model_name: str = None, tools_schema: Dict = None):
self._model_name = model_name
self._tools_schema = tools_schema or {}
self._dispatcher = ModelParserDispatcher(model_name=model_name, tools_schema=self._tools_schema)
def set_model_name(self, model_name: str) -> None:
"""Set the model name for model-specific parsing."""
self._model_name = model_name
# Recreate dispatcher with new model name
self._dispatcher = ModelParserDispatcher(model_name=model_name, tools_schema=self._tools_schema)
def extract_tool_calls(self, text: str, available_tools: List[Tool]) -> Optional[List[Dict]]:
"""Extract tool calls from model output using model-specific parsing."""
if not text:
return None
# Convert available_tools to the format expected by ModelParserDispatcher
tools_dict = {}
for tool in available_tools:
if hasattr(tool, 'function') and tool.function:
func = tool.function
tools_dict[func.name] = {
'description': func.description or '',
'parameters': func.parameters or {}
}
# Update dispatcher tools if changed
if tools_dict != self._tools_schema:
self._tools_schema = tools_dict
self._dispatcher.set_tools(tools_dict)
# Use the dispatcher to parse tool calls
tool_calls = self._dispatcher.parse(text)
if tool_calls:
# Add unique IDs to each tool call
import uuid
for tc in tool_calls:
if 'id' not in tc:
tc['id'] = f"call_{uuid.uuid4().hex[:16]}"
if 'type' not in tc:
tc['type'] = 'function'
return tool_calls
return None
def strip_tool_calls_from_content(self, text: str) -> str:
"""Remove tool call format from text after extracting tool calls."""
if not text:
return text
# Remove <tool>...</tool> and <function>...</function> patterns
text = re.sub(r'<tool>.*?</tool>', '', text, flags=re.DOTALL)
text = re.sub(r'<function>.*?</function>', '', text, flags=re.DOTALL)
# Remove JSON format with greedy matching: <tool>{...}</tool>
text = re.sub(r'<tool>\{.*?\}</tool>', '', text, flags=re.DOTALL)
text = re.sub(r'<function>\{.*?\}</function>', '', text, flags=re.DOTALL)
# More aggressive pattern
text = re.sub(r'<tool>[\s\S]*?</tool>', '', text)
text = re.sub(r'<function>[\s\S]*?</function>', '', text)
# Remove common tool name tags
for tool_name in ['read', 'write', 'exec', 'browser', 'message', 'web_search', 'web_fetch',
'memory_search', 'memory_get', 'sessions_list', 'sessions_send', 'tts', 'canvas', 'nodes',
'read_file', 'write_file', 'exec', 'process', 'browser', 'message', 'web_search', 'web_fetch',
'tts', 'canvas', 'nodes', 'agents_list', 'sessions_list', 'sessions_history', 'sessions_spawn',
'subagents', 'session_status', 'memory_search', 'memory_get']:
text = re.sub(rf'<{tool_name}>[\s\S]*?</{tool_name}>', '', text)
# Clean up excessive newlines
text = re.sub(r'\n{3,}', '\n\n', text)
return text
def format_tools_for_prompt(tools: List[Tool], messages: List[ChatMessage]) -> List[ChatMessage]:
"""Format tools into the system message or add a tool description."""
if not tools:
......@@ -2384,7 +2475,7 @@ class ModelManager:
def __init__(self):
self.backend: Optional[ModelBackend] = None
self.backend_type: Optional[str] = None
self.tool_parser = ToolCallParser()
self.tool_parser = ModelParserAdapter()
def _aggressive_vram_cleanup(self, model_manager):
"""
......@@ -2508,9 +2599,7 @@ class ModelManager:
# Load the model
self.backend.load_model(model_name, **kwargs)
self.tool_parser = ToolCallParser()
# Set model name on tool parser for model-specific parsing (e.g., Qwen)
self.tool_parser.set_model_name(model_name)
self.tool_parser = ModelParserAdapter(model_name=model_name)
def format_messages(self, messages: List[ChatMessage]) -> str:
"""Format messages into a prompt string."""
......@@ -2799,7 +2888,7 @@ class MultiModelManager:
self.tts_model: Optional[str] = None
self.image_models: List[str] = [] # List of image model names
self.vision_models: List[str] = [] # List of vision (image/video to text) model names
self.tool_parser = ToolCallParser()
self.tool_parser = ModelParserAdapter()
self.current_model_key: Optional[str] = None
# Configuration for each model type
self.config: Dict[str, Dict] = {}
......@@ -5109,7 +5198,7 @@ async def chat_completions(request: ChatCompletionRequest):
messages = format_tools_for_prompt(request.tools, messages)
# Get the tool_parser from the current manager
tool_parser = current_manager.tool_parser if hasattr(current_manager, 'tool_parser') else ToolCallParser()
tool_parser = current_manager.tool_parser if hasattr(current_manager, 'tool_parser') else ModelParserAdapter()
# Prepare stop sequences
stop_sequences = []
......
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