"""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. - must be a single-line valid JSON object (no line breaks inside). - Place multiple tool calls inside ONE element. - Do NOT use markdown code fences around the XML. - Do NOT output any inner monologue or explanation alongside the XML. TOOL_NAME {{"key": "value"}} WRONG (never do this): ```xml ... ``` I'll call the search tool now. ... {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 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 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 = [""] 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(" ") lines.append(f" {name}") lines.append(f" {args}") lines.append(" ") lines.append("") 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