| 1 | """Parse actions & format observations for OpenAI Responses API toolcalls"""
|
| 2 |
|
| 3 | import json
|
| 4 | import time
|
| 5 |
|
| 6 | from jinja2 import StrictUndefined, Template
|
| 7 |
|
| 8 | from minisweagent.exceptions import FormatError
|
| 9 |
|
| 10 | # OpenRouter/OpenAI Responses API uses a flat structure (no nested "function" key)
|
| 11 | BASH_TOOL_RESPONSE_API = {
|
| 12 | "type": "function",
|
| 13 | "name": "bash",
|
| 14 | "description": "Execute a bash command",
|
| 15 | "parameters": {
|
| 16 | "type": "object",
|
| 17 | "properties": {
|
| 18 | "command": {
|
| 19 | "type": "string",
|
| 20 | "description": "The bash command to execute",
|
| 21 | }
|
| 22 | },
|
| 23 | "required": ["command"],
|
| 24 | },
|
| 25 | }
|
| 26 |
|
| 27 |
|
| 28 | def _format_error_message(error_text: str) -> dict:
|
| 29 | """Create a FormatError message in Responses API format."""
|
| 30 | return {
|
| 31 | "type": "message",
|
| 32 | "role": "user",
|
| 33 | "content": [{"type": "input_text", "text": error_text}],
|
| 34 | "extra": {"interrupt_type": "FormatError"},
|
| 35 | }
|
| 36 |
|
| 37 |
|
| 38 | def parse_toolcall_actions_response(output: list, *, format_error_template: str) -> list[dict]:
|
| 39 | """Parse tool calls from a Responses API response output.
|
| 40 |
|
| 41 | Filters for function_call items and parses them.
|
| 42 | Response API format has name/arguments at top level with call_id:
|
| 43 | {"type": "function_call", "call_id": "...", "name": "bash", "arguments": "..."}
|
| 44 | """
|
| 45 | tool_calls = []
|
| 46 | for item in output:
|
| 47 | item_type = item.get("type") if isinstance(item, dict) else getattr(item, "type", None)
|
| 48 | if item_type == "function_call":
|
| 49 | tool_calls.append(
|
| 50 | item.model_dump() if hasattr(item, "model_dump") else dict(item) if not isinstance(item, dict) else item
|
| 51 | )
|
| 52 | if not tool_calls:
|
| 53 | error_text = Template(format_error_template, undefined=StrictUndefined).render(
|
| 54 | error="No tool calls found in the response. Every response MUST include at least one tool call.",
|
| 55 | )
|
| 56 | raise FormatError(_format_error_message(error_text))
|
| 57 | actions = []
|
| 58 | for tool_call in tool_calls:
|
| 59 | error_msg = ""
|
| 60 | args = {}
|
| 61 | try:
|
| 62 | args = json.loads(tool_call.get("arguments", "{}"))
|
| 63 | except Exception as e:
|
| 64 | error_msg = f"Error parsing tool call arguments: {e}. "
|
| 65 | if tool_call.get("name") != "bash":
|
| 66 | error_msg += f"Unknown tool '{tool_call.get('name')}'."
|
| 67 | if "command" not in args:
|
| 68 | error_msg += "Missing 'command' argument in bash tool call."
|
| 69 | if error_msg:
|
| 70 | error_text = Template(format_error_template, undefined=StrictUndefined).render(error=error_msg.strip())
|
| 71 | raise FormatError(_format_error_message(error_text))
|
| 72 | actions.append({"command": args["command"], "tool_call_id": tool_call.get("call_id") or tool_call.get("id")})
|
| 73 | return actions
|
| 74 |
|
| 75 |
|
| 76 | def format_toolcall_observation_messages(
|
| 77 | *,
|
| 78 | actions: list[dict],
|
| 79 | outputs: list[dict],
|
| 80 | observation_template: str,
|
| 81 | template_vars: dict | None = None,
|
| 82 | multimodal_regex: str = "",
|
| 83 | ) -> list[dict]:
|
| 84 | """Format execution outputs into function_call_output messages for Responses API."""
|
| 85 | not_executed = {"output": "", "returncode": -1, "exception_info": "action was not executed"}
|
| 86 | padded_outputs = outputs + [not_executed] * (len(actions) - len(outputs))
|
| 87 | results = []
|
| 88 | for action, output in zip(actions, padded_outputs):
|
| 89 | content = Template(observation_template, undefined=StrictUndefined).render(
|
| 90 | output=output, **(template_vars or {})
|
| 91 | )
|
| 92 | msg: dict = {
|
| 93 | "extra": {
|
| 94 | "raw_output": output.get("output", ""),
|
| 95 | "returncode": output.get("returncode"),
|
| 96 | "timestamp": time.time(),
|
| 97 | "exception_info": output.get("exception_info"),
|
| 98 | **output.get("extra", {}),
|
| 99 | },
|
| 100 | }
|
| 101 | if "tool_call_id" in action:
|
| 102 | msg["type"] = "function_call_output"
|
| 103 | msg["call_id"] = action["tool_call_id"]
|
| 104 | msg["output"] = content
|
| 105 | else: # human issued commands
|
| 106 | msg["type"] = "message"
|
| 107 | msg["role"] = "user"
|
| 108 | msg["content"] = [{"type": "input_text", "text": content}]
|
| 109 | results.append(msg)
|
| 110 | return results
|
| 111 |
|