File size: 6,095 Bytes
48d895c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Tool call prompt injection — convert OpenAI tools definitions into a

system-level instruction block that guides the model to output tool calls

in a structured XML format we can reliably parse.

"""

from __future__ import annotations

import json
from typing import Any

# ---------------------------------------------------------------------------
# Instruction template
# ---------------------------------------------------------------------------

_TOOL_SYSTEM_HEADER = """\

You have access to the following tools.



AVAILABLE TOOLS:

{tool_definitions}



TOOL CALL FORMAT — follow these rules exactly:

- When calling a tool, output ONLY the XML block below. No text before or after it.

- <parameters> must be a single-line valid JSON object (no line breaks inside).

- Place multiple tool calls inside ONE <tool_calls> element.

- Do NOT use markdown code fences around the XML.

- Do NOT output any inner monologue or explanation alongside the XML.



<tool_calls>

  <tool_call>

    <tool_name>TOOL_NAME</tool_name>

    <parameters>{{"key": "value"}}</parameters>

  </tool_call>

</tool_calls>



WRONG (never do this):

```xml

<tool_calls>...</tool_calls>

```

I'll call the search tool now. <tool_calls>...</tool_calls>



{tool_choice_instruction}

NOTE: Even if you believe you cannot fulfill the request, you must still follow the WHEN TO CALL rule above.\

"""

_CHOICE_AUTO     = "WHEN TO CALL: Call a tool when it is clearly needed. Otherwise respond in plain text."
_CHOICE_NONE     = "WHEN TO CALL: Do NOT call any tools. Respond in plain text only."
_CHOICE_REQUIRED = "WHEN TO CALL: You MUST output a <tool_calls> XML block. Do NOT write any plain-text reply. If you are uncertain, still call the most relevant tool with your best guess at the parameters."
_CHOICE_FORCED   = "WHEN TO CALL: You MUST output a <tool_calls> XML block calling the tool named \"{name}\". Do NOT write any plain-text reply under any circumstances."


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def build_tool_system_prompt(

    tools: list[dict[str, Any]],

    tool_choice: Any = None,

) -> str:
    """Return the full system-level instruction block to inject into the prompt.



    Args:

        tools: OpenAI-format tool definitions (list of {type, function:{name,description,parameters}}).

        tool_choice: OpenAI tool_choice value — "auto" | "none" | "required" |

                     {"type": "function", "function": {"name": "..."}}

    """
    tool_defs = _format_tool_definitions(tools)
    choice_instruction = _build_choice_instruction(tools, tool_choice)
    return _TOOL_SYSTEM_HEADER.format(
        tool_definitions=tool_defs,
        tool_choice_instruction=choice_instruction,
    )


def extract_tool_names(tools: list[dict[str, Any]]) -> list[str]:
    """Return the list of function names from an OpenAI tools array."""
    names: list[str] = []
    for tool in tools:
        func = tool.get("function") or {}
        name = func.get("name", "").strip()
        if name:
            names.append(name)
    return names


def inject_into_message(message: str, system_prompt: str) -> str:
    """Prepend the tool system prompt to the flattened message string."""
    return f"[system]: {system_prompt}\n\n{message}"


def tool_calls_to_xml(tool_calls: list[dict[str, Any]]) -> str:
    """Convert an OpenAI tool_calls array back into the XML format we use in

    prompts, so multi-turn conversations reconstruct context correctly."""
    lines = ["<tool_calls>"]
    for tc in tool_calls:
        func = tc.get("function") or {}
        name = func.get("name", "")
        args = func.get("arguments", "{}")
        # Normalise to single-line JSON
        try:
            args = json.dumps(json.loads(args), ensure_ascii=False, separators=(",", ":"))
        except (json.JSONDecodeError, ValueError, TypeError):
            pass
        lines.append("  <tool_call>")
        lines.append(f"    <tool_name>{name}</tool_name>")
        lines.append(f"    <parameters>{args}</parameters>")
        lines.append("  </tool_call>")
    lines.append("</tool_calls>")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------

def _format_tool_definitions(tools: list[dict[str, Any]]) -> str:
    parts: list[str] = []
    for tool in tools:
        func = tool.get("function") or {}
        name = func.get("name", "").strip()
        desc = (func.get("description") or "").strip()
        params = func.get("parameters")

        lines: list[str] = []
        lines.append(f"Tool: {name}")
        if desc:
            lines.append(f"Description: {desc}")
        if params:
            try:
                lines.append(f"Parameters: {json.dumps(params, ensure_ascii=False)}")
            except (TypeError, ValueError):
                lines.append(f"Parameters: {params}")
        parts.append("\n".join(lines))
    return "\n\n".join(parts)


def _build_choice_instruction(

    tools: list[dict[str, Any]],

    tool_choice: Any,

) -> str:
    if tool_choice is None or tool_choice == "auto":
        return _CHOICE_AUTO
    if tool_choice == "none":
        return _CHOICE_NONE
    if tool_choice == "required":
        return _CHOICE_REQUIRED
    # Object form: {"type": "function", "function": {"name": "..."}}
    if isinstance(tool_choice, dict):
        tc_type = tool_choice.get("type", "")
        if tc_type == "none":
            return _CHOICE_NONE
        if tc_type == "required":
            return _CHOICE_REQUIRED
        if tc_type == "function":
            forced_name = (tool_choice.get("function") or {}).get("name", "").strip()
            if forced_name:
                return _CHOICE_FORCED.format(name=forced_name)
    return _CHOICE_AUTO