File size: 3,105 Bytes
499ffe0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
"""
Tool/Function Calling Support for DarkSyntrix LLM Spaces.
Converts OpenAI tool definitions into system prompts and parses tool call responses.
"""

import json
import re
from typing import Optional


TOOL_CALL_TEMPLATE = """You have access to the following tools. When you need to use a tool, respond with a JSON object containing "tool_calls" array.

Available tools:
{tools_json}

To call a tool, respond with:
{{"tool_calls": [{{"id": "call_xxx", "type": "function", "function": {{"name": "tool_name", "arguments": {{...}}}}}}]}}

If you don't need a tool, respond normally with text.
"""


def build_tool_system_prompt(tools: list[dict]) -> str:
    """Build system prompt from OpenAI-format tools array."""
    if not tools:
        return ""
    simplified = []
    for tool in tools:
        fn = tool.get("function", tool)
        simplified.append({
            "name": fn.get("name", "unknown"),
            "description": fn.get("description", ""),
            "parameters": fn.get("parameters", {})
        })
    return TOOL_CALL_TEMPLATE.format(tools_json=json.dumps(simplified, indent=2))


TOOL_CALL_PATTERNS = [
    re.compile(r'(\{[\s\S]*?"tool_calls"[\s\S]*?\})', re.DOTALL),
    re.compile(r'<tool_call>([\s\S]*?)</tool_call>', re.DOTALL),
    re.compile(r'\[TOOL_CALL\]([\s\S]*?)\[\/TOOL_CALL\]', re.DOTALL),
    re.compile(r'Function call:\s*(\{[\s\S]*?["\']\})', re.DOTALL),
]


def parse_tool_calls(text: str) -> Optional[list[dict]]:
    """Parse tool calls from model output. Returns None if no tool calls found."""
    for pattern in TOOL_CALL_PATTERNS:
        matches = pattern.findall(text)
        for match in matches:
            try:
                parsed = json.loads(match.strip())
                tc = parsed.get("tool_calls", parsed.get("tool_call", []))
                if isinstance(tc, dict):
                    tc = [tc]
                if tc and isinstance(tc, list):
                    return tc
            except json.JSONDecodeError:
                continue

    try:
        parsed = json.loads(text.strip())
        tc = parsed.get("tool_calls", parsed.get("tool_call", []))
        if isinstance(tc, dict):
            tc = [tc]
        if tc and isinstance(tc, list):
            return tc
    except json.JSONDecodeError:
        pass

    return None


def strip_tool_calls(text: str) -> str:
    """Remove tool call JSON from text, keeping only natural language."""
    for pattern in TOOL_CALL_PATTERNS:
        text = pattern.sub("", text)
    text = re.sub(r'\s*<tool_call>.*?</tool_call>\s*', '', text, flags=re.DOTALL)
    text = re.sub(r'\s*\[TOOL_CALL\].*?\[\/TOOL_CALL\]\s*', '', text, flags=re.DOTALL)
    return text.strip()


def format_tool_call_response(tool_calls: list[dict]) -> str:
    """Format tool calls as OpenAI-compatible response delta."""
    return json.dumps({
        "choices": [{
            "index": 0,
            "delta": {
                "role": "assistant",
                "content": None,
                "tool_calls": tool_calls
            },
            "finish_reason": "tool_calls"
        }]
    })