Repository navigation
Expand file tree
/
Copy pathnative_protocol.py
More file actions
129 lines (103 loc) · 4.08 KB
/
Copy pathnative_protocol.py
File metadata and controls
129 lines (103 loc) · 4.08 KB
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
"""Shared text-action parsing, observations and loop control messages."""
# Portions adapted from mini-SWE-agent 2.4.6 (MIT); see THIRD_PARTY_NOTICES.
import re
import time
from typing import Any
class InterruptAgentFlow(Exception):
"""Raised to interrupt the agent flow and add messages."""
def __init__(self, *messages: dict):
self.messages = messages
super().__init__()
class Submitted(InterruptAgentFlow):
"""Raised when the agent has completed its task."""
class LimitsExceeded(InterruptAgentFlow):
"""Raised when the agent has exceeded its cost or step limit."""
class TimeExceeded(LimitsExceeded):
"""Raised when the agent has exceeded its wall-clock time limit."""
class UserInterruption(InterruptAgentFlow):
"""Raised when the user interrupts the agent."""
class FormatError(InterruptAgentFlow):
"""Raised when the LM's output is not in the expected format."""
UNSET = object()
def recursive_merge(*dictionaries: dict | None) -> dict:
"""Merge multiple dictionaries recursively.
Later dictionaries take precedence over earlier ones.
Nested dictionaries are merged recursively.
UNSET values are skipped.
"""
if not dictionaries:
return {}
result: dict[str, Any] = {}
for d in dictionaries:
if d is None:
continue
for key, value in d.items():
if value is UNSET:
continue
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = recursive_merge(result[key], value)
elif isinstance(value, dict):
# Recursively merge dict values to filter out nested UNSET values
result[key] = recursive_merge(value)
else:
result[key] = value
return result
def parse_regex_actions(
content: str,
*,
action_regex: str,
format_error_template: str,
template_kwargs: dict | None = None,
) -> list[dict]:
"""Parse actions from text content using regex. Raises FormatError if not exactly one action.
``template_kwargs`` are extra variables exposed to ``format_error_template`` (e.g.
``{"finish_reason": ...}`` so a template can report a ``max_tokens`` truncation -- which shows
up here as zero parsed actions -- instead of a generic format error).
"""
from jinja2 import StrictUndefined, Template
actions = [a.strip() for a in re.findall(action_regex, content, re.DOTALL)]
if len(actions) != 1:
error_msg = f"Expected exactly 1 action, found {len(actions)}."
raise FormatError(
{
"role": "user",
"content": Template(format_error_template, undefined=StrictUndefined).render(
actions=actions, error=error_msg, **(template_kwargs or {})
),
"extra": {
"interrupt_type": "FormatError",
"n_actions": len(actions),
"model_response": content,
},
}
)
return [{"command": action} for action in actions]
def format_observation_messages(
outputs: list[dict],
*,
observation_template: str,
template_vars: dict | None = None,
multimodal_regex: str = "",
) -> list[dict]:
"""Format execution outputs into user observation messages."""
from jinja2 import StrictUndefined, Template
if multimodal_regex:
raise ValueError("Native execution supports text observations only")
results = []
for output in outputs:
content = Template(observation_template, undefined=StrictUndefined).render(
output=output, **(template_vars or {})
)
msg: dict = {
"role": "user",
"content": content,
"extra": {
"raw_output": output.get("output", ""),
"returncode": output.get("returncode"),
"timestamp": time.time(),
"exception_info": output.get("exception_info"),
**output.get("extra", {}),
},
}
results.append(msg)
return results