mirror of
https://github.com/zenlm/zen-eco.git
synced 2026-10-30 14:58:11 +00:00
287 lines
9.5 KiB
Python
287 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Qwen3-4B Tool Calling Integration for Zen-Eco Agent
|
|
Based on Manojb/Qwen3-4b-toolcall-gguf-llamacpp-codex
|
|
Fine-tuned on Salesforce/xlam-function-calling-60k dataset
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from typing import List, Dict, Any, Optional, Tuple
|
|
from pathlib import Path
|
|
import logging
|
|
from llama_cpp import Llama
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Qwen3ToolCaller:
|
|
"""
|
|
Zen-Eco Agent Tool Calling with Qwen3-4B
|
|
Model: Manojb/Qwen3-4b-toolcall-gguf-llamacpp-codex
|
|
Dataset: Salesforce/xlam-function-calling-60k
|
|
"""
|
|
|
|
MODEL_INFO = {
|
|
"name": "Qwen3-4B-Function-Calling-Pro",
|
|
"base_model": "Qwen/Qwen3-4B-Instruct",
|
|
"dataset": "Salesforce/xlam-function-calling-60k",
|
|
"dataset_url": "https://huggingface.co/datasets/Salesforce/xlam-function-calling-60k",
|
|
"model_url": "https://huggingface.co/Manojb/Qwen3-4b-toolcall-gguf-llamacpp-codex",
|
|
"quantization": "Q8_0",
|
|
"parameters": "4B",
|
|
"context_length": 262144,
|
|
"architecture": "Qwen3"
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
model_path: Optional[str] = None,
|
|
n_ctx: int = 8192,
|
|
n_threads: int = 8,
|
|
n_batch: int = 512,
|
|
n_gpu_layers: int = -1,
|
|
temperature: float = 0.7,
|
|
top_p: float = 0.9,
|
|
verbose: bool = False
|
|
):
|
|
"""
|
|
Initialize Qwen3 Tool Caller
|
|
|
|
Args:
|
|
model_path: Path to GGUF model file
|
|
n_ctx: Context window size
|
|
n_threads: Number of CPU threads
|
|
n_batch: Batch size for prompt processing
|
|
n_gpu_layers: Number of layers to offload to GPU (-1 for all)
|
|
temperature: Sampling temperature
|
|
top_p: Nucleus sampling threshold
|
|
verbose: Enable verbose logging
|
|
"""
|
|
if model_path is None:
|
|
model_path = str(Path.home() / "work/zen/zen-eco/agent/gguf/Qwen3-4B-Function-Calling-Pro.gguf")
|
|
|
|
self.model_path = model_path
|
|
self.temperature = temperature
|
|
self.top_p = top_p
|
|
|
|
logger.info(f"Loading Qwen3-4B Tool Calling model from {model_path}")
|
|
logger.info(f"Model trained on: {self.MODEL_INFO['dataset']}")
|
|
|
|
self.llm = Llama(
|
|
model_path=model_path,
|
|
n_ctx=n_ctx,
|
|
n_threads=n_threads,
|
|
n_batch=n_batch,
|
|
n_gpu_layers=n_gpu_layers,
|
|
verbose=verbose,
|
|
seed=-1
|
|
)
|
|
|
|
def format_tools(self, tools: List[Dict[str, Any]]) -> str:
|
|
"""
|
|
Format tools for the model prompt
|
|
|
|
Args:
|
|
tools: List of tool definitions
|
|
|
|
Returns:
|
|
Formatted tool string
|
|
"""
|
|
tool_descriptions = []
|
|
for tool in tools:
|
|
if "function" in tool:
|
|
func = tool["function"]
|
|
tool_desc = f"- {func['name']}: {func['description']}"
|
|
if "parameters" in func:
|
|
params = func["parameters"]
|
|
if "properties" in params:
|
|
param_list = []
|
|
for prop, details in params["properties"].items():
|
|
required = prop in params.get("required", [])
|
|
param_list.append(
|
|
f" - {prop} ({details.get('type', 'string')})"
|
|
f"{' [required]' if required else ''}: "
|
|
f"{details.get('description', '')}"
|
|
)
|
|
if param_list:
|
|
tool_desc += "\n" + "\n".join(param_list)
|
|
tool_descriptions.append(tool_desc)
|
|
|
|
return "\n".join(tool_descriptions)
|
|
|
|
def extract_tool_calls(self, text: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Extract tool calls from model response
|
|
|
|
Args:
|
|
text: Model response text
|
|
|
|
Returns:
|
|
List of extracted tool calls
|
|
"""
|
|
tool_calls = []
|
|
|
|
# Pattern 1: JSON array format
|
|
json_array_pattern = r'\[(?:[^[\]]*|\[.*?\])*\]'
|
|
matches = re.findall(json_array_pattern, text, re.DOTALL)
|
|
|
|
for match in matches:
|
|
try:
|
|
parsed = json.loads(match)
|
|
if isinstance(parsed, list):
|
|
for item in parsed:
|
|
if isinstance(item, dict) and 'name' in item:
|
|
tool_call = {
|
|
'name': item['name'],
|
|
'arguments': item.get('arguments', item.get('parameters', {}))
|
|
}
|
|
tool_calls.append(tool_call)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Pattern 2: Function call format
|
|
func_pattern = r'<function_call>\s*({[^}]+})\s*</function_call>'
|
|
func_matches = re.findall(func_pattern, text, re.DOTALL)
|
|
|
|
for match in func_matches:
|
|
try:
|
|
parsed = json.loads(match)
|
|
if 'name' in parsed:
|
|
tool_calls.append(parsed)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Pattern 3: Direct function notation
|
|
direct_pattern = r'(\w+)\((.*?)\)'
|
|
if not tool_calls:
|
|
direct_matches = re.findall(direct_pattern, text)
|
|
for func_name, args_str in direct_matches:
|
|
if func_name in ['get_weather', 'search_hotels', 'calculate', 'search', 'execute']:
|
|
try:
|
|
# Try to parse arguments
|
|
if args_str:
|
|
args = {}
|
|
for arg in args_str.split(','):
|
|
if '=' in arg:
|
|
key, val = arg.split('=', 1)
|
|
args[key.strip()] = val.strip().strip('"\'')
|
|
tool_calls.append({'name': func_name, 'arguments': args})
|
|
except:
|
|
pass
|
|
|
|
return tool_calls
|
|
|
|
def generate(
|
|
self,
|
|
messages: List[Dict[str, str]],
|
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
max_tokens: int = 1024,
|
|
temperature: Optional[float] = None,
|
|
top_p: Optional[float] = None
|
|
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
"""
|
|
Generate response with tool calling
|
|
|
|
Args:
|
|
messages: Chat messages
|
|
tools: Available tools
|
|
max_tokens: Maximum tokens to generate
|
|
temperature: Override default temperature
|
|
top_p: Override default top_p
|
|
|
|
Returns:
|
|
Tuple of (response text, tool calls)
|
|
"""
|
|
# Build prompt
|
|
prompt_parts = []
|
|
|
|
# Add system message with tool descriptions
|
|
if tools:
|
|
tool_desc = self.format_tools(tools)
|
|
system_msg = (
|
|
"You are a helpful AI assistant with access to the following tools:\n\n"
|
|
f"{tool_desc}\n\n"
|
|
"When you need to use a tool, format your response as:\n"
|
|
"[{\"name\": \"tool_name\", \"arguments\": {\"param1\": \"value1\"}}]\n"
|
|
"You can call multiple tools in a single response."
|
|
)
|
|
prompt_parts.append(f"<|im_start|>system\n{system_msg}<|im_end|>")
|
|
|
|
# Add message history
|
|
for msg in messages:
|
|
role = msg.get("role", "user")
|
|
content = msg.get("content", "")
|
|
prompt_parts.append(f"<|im_start|>{role}\n{content}<|im_end|>")
|
|
|
|
# Add assistant prompt
|
|
prompt_parts.append("<|im_start|>assistant\n")
|
|
|
|
prompt = "\n".join(prompt_parts)
|
|
|
|
# Generate response
|
|
response = self.llm(
|
|
prompt,
|
|
max_tokens=max_tokens,
|
|
temperature=temperature or self.temperature,
|
|
top_p=top_p or self.top_p,
|
|
stop=["<|im_end|>", "<|im_start|>"],
|
|
echo=False
|
|
)
|
|
|
|
response_text = response['choices'][0]['text']
|
|
tool_calls = self.extract_tool_calls(response_text)
|
|
|
|
return response_text, tool_calls
|
|
|
|
def chat(
|
|
self,
|
|
message: str,
|
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
system_message: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Simple chat interface
|
|
|
|
Args:
|
|
message: User message
|
|
tools: Available tools
|
|
system_message: Optional system message
|
|
|
|
Returns:
|
|
Response dictionary with text and tool calls
|
|
"""
|
|
messages = []
|
|
if system_message:
|
|
messages.append({"role": "system", "content": system_message})
|
|
messages.append({"role": "user", "content": message})
|
|
|
|
response_text, tool_calls = self.generate(messages, tools)
|
|
|
|
return {
|
|
'response': response_text,
|
|
'tool_calls': tool_calls,
|
|
'model_info': self.MODEL_INFO
|
|
}
|
|
|
|
@classmethod
|
|
def from_pretrained(cls, model_id: str = "Manojb/Qwen3-4b-toolcall-gguf-llamacpp-codex", **kwargs):
|
|
"""
|
|
Load model from HuggingFace hub
|
|
|
|
Args:
|
|
model_id: HuggingFace model ID
|
|
**kwargs: Additional arguments for initialization
|
|
|
|
Returns:
|
|
Initialized Qwen3ToolCaller instance
|
|
"""
|
|
from huggingface_hub import hf_hub_download
|
|
|
|
# Download the model
|
|
model_path = hf_hub_download(
|
|
repo_id=model_id,
|
|
filename="Qwen3-4B-Function-Calling-Pro.gguf",
|
|
local_dir=Path.home() / "work/zen/zen-eco/agent/gguf"
|
|
)
|
|
|
|
return cls(model_path=model_path, **kwargs) |