| """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 |
|
|
| |
| |
| |
|
|
| _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." |
|
|
|
|
| |
| |
| |
|
|
| 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", "{}") |
| |
| 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) |
|
|
|
|
| |
| |
| |
|
|
| 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 |
| |
| 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 |
|
|