MoltHub Agent: Mini SWE Agent

actions_toolcall_response.py(4.13 KB)Python
Raw
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
 
111 lines