autobyteus 1.1.6__py3-none-any.whl → 1.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autobyteus/agent/context/agent_runtime_state.py +7 -1
- autobyteus/agent/handlers/tool_result_event_handler.py +100 -88
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +7 -1
- autobyteus/agent/tool_invocation.py +25 -1
- autobyteus/agent_team/agent_team_builder.py +22 -1
- autobyteus/agent_team/context/agent_team_runtime_state.py +0 -2
- autobyteus/llm/llm_factory.py +25 -57
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/token_counter_factory.py +2 -0
- autobyteus/multimedia/audio/audio_model.py +2 -1
- autobyteus/multimedia/image/image_model.py +2 -1
- autobyteus/task_management/tools/publish_task_plan.py +4 -16
- autobyteus/task_management/tools/update_task_status.py +4 -19
- autobyteus/tools/__init__.py +2 -4
- autobyteus/tools/base_tool.py +98 -29
- autobyteus/tools/browser/standalone/__init__.py +0 -1
- autobyteus/tools/google_search.py +149 -0
- autobyteus/tools/mcp/schema_mapper.py +29 -71
- autobyteus/tools/multimedia/audio_tools.py +3 -3
- autobyteus/tools/multimedia/image_tools.py +5 -5
- autobyteus/tools/parameter_schema.py +82 -89
- autobyteus/tools/pydantic_schema_converter.py +81 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +89 -20
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +115 -41
- autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +50 -20
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +55 -22
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +54 -21
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +53 -23
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +270 -94
- autobyteus/tools/usage/providers/tool_manifest_provider.py +39 -14
- autobyteus-1.1.7.dist-info/METADATA +204 -0
- {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/RECORD +39 -40
- examples/run_google_slides_agent.py +2 -2
- examples/run_mcp_google_slides_client.py +1 -1
- examples/run_sqlite_agent.py +1 -1
- autobyteus/tools/ask_user_input.py +0 -40
- autobyteus/tools/browser/standalone/factory/google_search_factory.py +0 -25
- autobyteus/tools/browser/standalone/google_search_ui.py +0 -126
- autobyteus-1.1.6.dist-info/METADATA +0 -161
- {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,126 +1,302 @@
|
|
|
1
|
-
import xml.etree.ElementTree as ET
|
|
2
|
-
import re
|
|
3
|
-
import uuid
|
|
4
|
-
import html
|
|
5
|
-
from xml.sax.saxutils import escape
|
|
6
|
-
import xml.parsers.expat
|
|
7
1
|
import logging
|
|
2
|
+
import re
|
|
8
3
|
from typing import TYPE_CHECKING, Dict, Any, List
|
|
4
|
+
from dataclasses import dataclass, field
|
|
9
5
|
|
|
10
6
|
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
11
7
|
from .base_parser import BaseToolUsageParser
|
|
12
|
-
from .exceptions import ToolUsageParseException
|
|
13
8
|
|
|
14
9
|
if TYPE_CHECKING:
|
|
15
10
|
from autobyteus.llm.utils.response_types import CompleteResponse
|
|
16
11
|
|
|
17
12
|
logger = logging.getLogger(__name__)
|
|
18
13
|
|
|
14
|
+
# A unique UUID to use as an internal key for storing text content.
|
|
15
|
+
# This prevents any potential collision with user-provided argument names.
|
|
16
|
+
_INTERNAL_TEXT_KEY_UUID = "4e1a3b1e-3b2a-4d3c-9a8b-2a1c2b3d4e5f"
|
|
17
|
+
|
|
18
|
+
# --- Internal Arguments Parser with State Machine ---
|
|
19
|
+
# This entire section is now encapsulated in its own class for clarity.
|
|
20
|
+
|
|
21
|
+
class _XmlArgumentsParser:
|
|
22
|
+
"""
|
|
23
|
+
A dedicated parser for the XML content within an <arguments> tag.
|
|
24
|
+
It encapsulates the state machine and all related logic, separating it
|
|
25
|
+
from the higher-level tool-finding logic.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# --- Nested State Machine Components ---
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class _ParsingContext:
|
|
32
|
+
"""Holds the shared state for the parsing process."""
|
|
33
|
+
parser: '_XmlArgumentsParser'
|
|
34
|
+
input_string: str
|
|
35
|
+
cursor: int = 0
|
|
36
|
+
stack: List[Any] = field(default_factory=list)
|
|
37
|
+
content_buffer: str = ""
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
self.stack.append({}) # Root of arguments is a dictionary
|
|
41
|
+
|
|
42
|
+
def is_eof(self) -> bool:
|
|
43
|
+
return self.cursor >= len(self.input_string)
|
|
44
|
+
|
|
45
|
+
def append_to_buffer(self, text: str):
|
|
46
|
+
self.content_buffer += text
|
|
47
|
+
|
|
48
|
+
def commit_content_buffer(self):
|
|
49
|
+
if self.content_buffer:
|
|
50
|
+
self.parser._commit_content(self.stack, self.content_buffer)
|
|
51
|
+
self.content_buffer = ""
|
|
52
|
+
|
|
53
|
+
class _ParserState:
|
|
54
|
+
"""Abstract base class for a state in our parser's state machine."""
|
|
55
|
+
def handle(self, context: '_XmlArgumentsParser._ParsingContext') -> '_XmlArgumentsParser._ParserState':
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
class _ParsingContentState(_ParserState):
|
|
59
|
+
"""Handles accumulation of character data between tags."""
|
|
60
|
+
def handle(self, context: '_XmlArgumentsParser._ParsingContext') -> '_XmlArgumentsParser._ParserState':
|
|
61
|
+
if context.is_eof():
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
next_tag_start = context.input_string.find('<', context.cursor)
|
|
65
|
+
|
|
66
|
+
if next_tag_start == -1:
|
|
67
|
+
context.append_to_buffer(context.input_string[context.cursor:])
|
|
68
|
+
context.cursor = len(context.input_string)
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
is_valid_tag = False
|
|
72
|
+
if next_tag_start + 1 < len(context.input_string):
|
|
73
|
+
next_char = context.input_string[next_tag_start + 1]
|
|
74
|
+
if next_char.isalpha() or next_char == '/':
|
|
75
|
+
is_valid_tag = True
|
|
76
|
+
|
|
77
|
+
if is_valid_tag:
|
|
78
|
+
content_before_tag = context.input_string[context.cursor:next_tag_start]
|
|
79
|
+
context.append_to_buffer(content_before_tag)
|
|
80
|
+
context.commit_content_buffer()
|
|
81
|
+
context.cursor = next_tag_start
|
|
82
|
+
return self.parser._ParsingTagState(self.parser)
|
|
83
|
+
else:
|
|
84
|
+
content_with_char = context.input_string[context.cursor : next_tag_start + 1]
|
|
85
|
+
context.append_to_buffer(content_with_char)
|
|
86
|
+
context.cursor = next_tag_start + 1
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __init__(self, parser: '_XmlArgumentsParser'):
|
|
90
|
+
self.parser = parser
|
|
91
|
+
|
|
92
|
+
class _ParsingTagState(_ParserState):
|
|
93
|
+
"""Handles parsing of a tag, from '<' to '>'."""
|
|
94
|
+
def handle(self, context: '_XmlArgumentsParser._ParsingContext') -> '_XmlArgumentsParser._ParserState':
|
|
95
|
+
tag_content_end = context.input_string.find('>', context.cursor)
|
|
96
|
+
if tag_content_end == -1:
|
|
97
|
+
context.append_to_buffer(context.input_string[context.cursor:])
|
|
98
|
+
context.cursor = len(context.input_string)
|
|
99
|
+
return self.parser._ParsingContentState(self.parser)
|
|
100
|
+
|
|
101
|
+
tag_content = context.input_string[context.cursor + 1 : tag_content_end]
|
|
102
|
+
context.parser.process_tag(tag_content, context)
|
|
103
|
+
|
|
104
|
+
context.cursor = tag_content_end + 1
|
|
105
|
+
return self.parser._ParsingContentState(self.parser)
|
|
106
|
+
|
|
107
|
+
def __init__(self, parser: '_XmlArgumentsParser'):
|
|
108
|
+
self.parser = parser
|
|
109
|
+
|
|
110
|
+
# --- Parser Implementation ---
|
|
111
|
+
|
|
112
|
+
def __init__(self, xml_string: str):
|
|
113
|
+
self.xml_string = xml_string
|
|
114
|
+
|
|
115
|
+
def parse(self) -> Dict[str, Any]:
|
|
116
|
+
"""Drives the state machine to parse the XML string."""
|
|
117
|
+
context = self._ParsingContext(parser=self, input_string=self.xml_string)
|
|
118
|
+
state = self._ParsingContentState(self)
|
|
119
|
+
|
|
120
|
+
while state and not context.is_eof():
|
|
121
|
+
state = state.handle(context)
|
|
122
|
+
|
|
123
|
+
context.commit_content_buffer()
|
|
124
|
+
|
|
125
|
+
final_args = context.stack[0]
|
|
126
|
+
self._cleanup_internal_keys(final_args)
|
|
127
|
+
return final_args
|
|
128
|
+
|
|
129
|
+
def process_tag(self, tag_content: str, context: '_ParsingContext'):
|
|
130
|
+
STRUCTURAL_TAGS = {'arg', 'item'}
|
|
131
|
+
stripped_content = tag_content.strip()
|
|
132
|
+
if not stripped_content:
|
|
133
|
+
context.append_to_buffer(f"<{tag_content}>")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
is_closing = stripped_content.startswith('/')
|
|
137
|
+
tag_name = (stripped_content[1:] if is_closing else stripped_content).split(' ')[0]
|
|
138
|
+
|
|
139
|
+
if tag_name in STRUCTURAL_TAGS:
|
|
140
|
+
if is_closing:
|
|
141
|
+
self._handle_closing_tag(context.stack)
|
|
142
|
+
else:
|
|
143
|
+
self._handle_opening_tag(context.stack, tag_content)
|
|
144
|
+
else:
|
|
145
|
+
context.append_to_buffer(f"<{tag_content}>")
|
|
146
|
+
|
|
147
|
+
def _commit_content(self, stack: List[Any], content: str):
|
|
148
|
+
trimmed_content = content.strip()
|
|
149
|
+
if not trimmed_content and '<' not in content and '>' not in content:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
top = stack[-1]
|
|
153
|
+
if isinstance(top, dict):
|
|
154
|
+
top[_INTERNAL_TEXT_KEY_UUID] = top.get(_INTERNAL_TEXT_KEY_UUID, '') + content
|
|
155
|
+
|
|
156
|
+
def _handle_opening_tag(self, stack: List[Any], tag_content: str):
|
|
157
|
+
parent = stack[-1]
|
|
158
|
+
|
|
159
|
+
if tag_content.strip().startswith('arg'):
|
|
160
|
+
name_match = re.search(r'name="([^"]+)"', tag_content)
|
|
161
|
+
if name_match and isinstance(parent, dict):
|
|
162
|
+
arg_name = name_match.group(1)
|
|
163
|
+
new_container = {}
|
|
164
|
+
parent[arg_name] = new_container
|
|
165
|
+
stack.append(new_container)
|
|
166
|
+
|
|
167
|
+
elif tag_content.strip().startswith('item'):
|
|
168
|
+
if isinstance(parent, dict):
|
|
169
|
+
grandparent = stack[-2]
|
|
170
|
+
parent_key = next((k for k, v in grandparent.items() if v is parent), None)
|
|
171
|
+
if parent_key:
|
|
172
|
+
new_list = []
|
|
173
|
+
grandparent[parent_key] = new_list
|
|
174
|
+
stack[-1] = new_list
|
|
175
|
+
parent = new_list
|
|
176
|
+
|
|
177
|
+
if isinstance(parent, list):
|
|
178
|
+
new_item_container = {}
|
|
179
|
+
parent.append(new_item_container)
|
|
180
|
+
stack.append(new_item_container)
|
|
181
|
+
|
|
182
|
+
def _handle_closing_tag(self, stack: List[Any]):
|
|
183
|
+
if len(stack) > 1:
|
|
184
|
+
top = stack.pop()
|
|
185
|
+
parent = stack[-1]
|
|
186
|
+
|
|
187
|
+
is_primitive = False
|
|
188
|
+
if isinstance(top, dict):
|
|
189
|
+
keys = top.keys()
|
|
190
|
+
if not keys or (len(keys) == 1 and _INTERNAL_TEXT_KEY_UUID in keys):
|
|
191
|
+
is_primitive = True
|
|
192
|
+
|
|
193
|
+
if is_primitive:
|
|
194
|
+
value = top.get(_INTERNAL_TEXT_KEY_UUID, '')
|
|
195
|
+
if isinstance(parent, list):
|
|
196
|
+
try:
|
|
197
|
+
idx = parent.index(top)
|
|
198
|
+
parent[idx] = value
|
|
199
|
+
except ValueError:
|
|
200
|
+
logger.warning("Could not find item to collapse in parent list.")
|
|
201
|
+
elif isinstance(parent, dict):
|
|
202
|
+
parent_key = next((k for k, v in parent.items() if v is top), None)
|
|
203
|
+
if parent_key:
|
|
204
|
+
parent[parent_key] = value
|
|
205
|
+
|
|
206
|
+
def _cleanup_internal_keys(self, data: Any):
|
|
207
|
+
if isinstance(data, dict):
|
|
208
|
+
if _INTERNAL_TEXT_KEY_UUID in data and len(data) > 1:
|
|
209
|
+
del data[_INTERNAL_TEXT_KEY_UUID]
|
|
210
|
+
for value in data.values():
|
|
211
|
+
self._cleanup_internal_keys(value)
|
|
212
|
+
elif isinstance(data, list):
|
|
213
|
+
for item in data:
|
|
214
|
+
self._cleanup_internal_keys(item)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# --- Main Parser Class ---
|
|
218
|
+
|
|
19
219
|
class DefaultXmlToolUsageParser(BaseToolUsageParser):
|
|
20
220
|
"""
|
|
21
|
-
Parses LLM responses for tool usage commands formatted as XML
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
conversational text, malformed XML, or other noise.
|
|
221
|
+
Parses LLM responses for tool usage commands formatted as XML.
|
|
222
|
+
This class is responsible for finding <tool> blocks and delegating the
|
|
223
|
+
parsing of their arguments to the specialized _XmlArgumentsParser.
|
|
25
224
|
"""
|
|
225
|
+
|
|
26
226
|
def get_name(self) -> str:
|
|
27
227
|
return "default_xml_tool_usage_parser"
|
|
28
228
|
|
|
29
229
|
def parse(self, response: 'CompleteResponse') -> List[ToolInvocation]:
|
|
30
230
|
text = response.content
|
|
31
231
|
invocations: List[ToolInvocation] = []
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
while
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
break # No more tool tags in the rest of the string
|
|
39
|
-
|
|
40
|
-
# Find the end of that opening <tool ...> tag. This is a potential end.
|
|
41
|
-
tool_start_tag_end = text.find('>', tool_start_index)
|
|
42
|
-
if tool_start_tag_end == -1:
|
|
43
|
-
# Incomplete tag at the end of the file, break
|
|
232
|
+
i = 0
|
|
233
|
+
|
|
234
|
+
while i < len(text):
|
|
235
|
+
try:
|
|
236
|
+
i = text.index('<tool', i)
|
|
237
|
+
except ValueError:
|
|
44
238
|
break
|
|
45
239
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
next_opening_bracket = text.find('<', tool_start_index + 1)
|
|
49
|
-
if next_opening_bracket != -1 and next_opening_bracket < tool_start_tag_end:
|
|
50
|
-
# The tag was not closed properly before another one started.
|
|
51
|
-
# Advance the cursor to this new tag and restart the loop.
|
|
52
|
-
cursor = next_opening_bracket
|
|
53
|
-
continue
|
|
240
|
+
open_tag_end = text.find('>', i)
|
|
241
|
+
if open_tag_end == -1: break
|
|
54
242
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
cursor = tool_start_tag_end + 1
|
|
243
|
+
open_tag_content = text[i:open_tag_end+1]
|
|
244
|
+
name_match = re.search(r'name="([^"]+)"', open_tag_content)
|
|
245
|
+
if not name_match:
|
|
246
|
+
i = open_tag_end + 1
|
|
60
247
|
continue
|
|
248
|
+
|
|
249
|
+
tool_name = name_match.group(1)
|
|
250
|
+
logger.debug(f"--- Found tool '{tool_name}' at index {i} ---")
|
|
61
251
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
252
|
+
cursor = open_tag_end + 1
|
|
253
|
+
nesting_level = 1
|
|
254
|
+
content_end = -1
|
|
65
255
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# and let the loop find the inner tag on the next iteration.
|
|
70
|
-
# This check is now more of a safeguard, as the logic above should handle most cases.
|
|
71
|
-
if '<tool' in tool_block[1:]:
|
|
72
|
-
# Advance cursor past the opening tag of this malformed block to continue scanning
|
|
73
|
-
cursor = tool_start_tag_end + 1
|
|
74
|
-
continue
|
|
256
|
+
while cursor < len(text):
|
|
257
|
+
next_open = text.find('<tool', cursor)
|
|
258
|
+
next_close = text.find('</tool>', cursor)
|
|
75
259
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if not tool_name:
|
|
84
|
-
logger.warning(f"Skipping a <tool> block with no name attribute: {processed_block[:100]}")
|
|
260
|
+
if next_close == -1: break
|
|
261
|
+
|
|
262
|
+
if next_open != -1 and next_open < next_close:
|
|
263
|
+
nesting_level += 1
|
|
264
|
+
end_of_nested_open = text.find('>', next_open)
|
|
265
|
+
if end_of_nested_open == -1: break
|
|
266
|
+
cursor = end_of_nested_open + 1
|
|
85
267
|
else:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
logger.warning(f"Skipping malformed XML tool block: {e}")
|
|
268
|
+
nesting_level -= 1
|
|
269
|
+
if nesting_level == 0:
|
|
270
|
+
content_end = next_close
|
|
271
|
+
break
|
|
272
|
+
cursor = next_close + len('</tool>')
|
|
273
|
+
|
|
274
|
+
if content_end == -1:
|
|
275
|
+
logger.warning(f"Malformed XML for tool '{tool_name}': could not find matching </tool> tag.")
|
|
276
|
+
i = open_tag_end + 1
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
tool_content = text[open_tag_end+1:content_end]
|
|
280
|
+
args_match = re.search(r'<arguments>(.*)</arguments>', tool_content, re.DOTALL)
|
|
100
281
|
|
|
101
|
-
|
|
102
|
-
|
|
282
|
+
arguments = {}
|
|
283
|
+
if args_match:
|
|
284
|
+
arguments_xml = args_match.group(1)
|
|
285
|
+
try:
|
|
286
|
+
# Delegate parsing to the specialized class
|
|
287
|
+
arguments = self._parse_arguments(arguments_xml)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Arguments parser failed for tool '{tool_name}': {e}", exc_info=True)
|
|
103
290
|
|
|
291
|
+
invocations.append(ToolInvocation(name=tool_name, arguments=arguments))
|
|
292
|
+
i = content_end + len('</tool>')
|
|
293
|
+
|
|
104
294
|
return invocations
|
|
105
295
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
arguments: Dict[str, Any] = {}
|
|
114
|
-
arguments_container = command_element.find('arguments')
|
|
115
|
-
if arguments_container is None:
|
|
116
|
-
return arguments
|
|
117
|
-
|
|
118
|
-
for arg_element in arguments_container.findall('arg'):
|
|
119
|
-
arg_name = arg_element.attrib.get('name')
|
|
120
|
-
if arg_name:
|
|
121
|
-
# Use .text to get only the direct text content of the tag.
|
|
122
|
-
# This is safer than itertext() if the LLM hallucinates nested tags.
|
|
123
|
-
# The XML parser already handles unescaping of standard entities.
|
|
124
|
-
raw_text = arg_element.text or ""
|
|
125
|
-
arguments[arg_name] = raw_text
|
|
126
|
-
return arguments
|
|
296
|
+
def _parse_arguments(self, xml_string: str) -> Dict[str, Any]:
|
|
297
|
+
"""
|
|
298
|
+
Delegates parsing of an arguments XML string to the dedicated parser class.
|
|
299
|
+
"""
|
|
300
|
+
parser = _XmlArgumentsParser(xml_string)
|
|
301
|
+
return parser.parse()
|
|
302
|
+
|
|
@@ -16,14 +16,36 @@ class ToolManifestProvider:
|
|
|
16
16
|
"""
|
|
17
17
|
Generates a complete tool manifest string, which includes the schema
|
|
18
18
|
and an example for each provided tool. This is suitable for injection
|
|
19
|
+
|
|
19
20
|
into a system prompt. It uses the central ToolFormattingRegistry to get
|
|
20
21
|
the correct formatters for the specified provider.
|
|
21
22
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# --- XML Specific Headers and Guidelines ---
|
|
24
|
+
XML_SCHEMA_HEADER = "## Tool Definition:"
|
|
25
|
+
XML_EXAMPLE_HEADER = "## Tool Usage Examples and Guidelines:"
|
|
26
|
+
XML_GENERAL_GUIDELINES = (
|
|
27
|
+
"To use this tool, you must construct an XML block exactly like the examples below. "
|
|
28
|
+
"Ensure all tags are correctly named and nested. Pay close attention to how arguments, "
|
|
29
|
+
"especially complex ones like lists and objects, are formatted."
|
|
30
|
+
)
|
|
31
|
+
XML_ARRAY_GUIDELINES = (
|
|
32
|
+
"Formatting Lists/Arrays: For any argument that is a list (an array), you MUST wrap each "
|
|
33
|
+
"individual value in its own `<item>` tag. Do not use comma-separated strings or JSON-style `[...]` arrays within a single tag.\n\n"
|
|
34
|
+
"Correct:\n"
|
|
35
|
+
'<arg name="dependencies">\n'
|
|
36
|
+
' <item>task_1</item>\n'
|
|
37
|
+
' <item>task_2</item>\n'
|
|
38
|
+
'</arg>\n\n'
|
|
39
|
+
"Incorrect:\n"
|
|
40
|
+
'<arg name="dependencies">[task_1, task_2]</arg>\n'
|
|
41
|
+
'<arg name="dependencies">task_1, task_2</arg>'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# --- JSON Specific Headers ---
|
|
45
|
+
JSON_SCHEMA_HEADER = "## Tool Definition:"
|
|
25
46
|
JSON_EXAMPLE_HEADER = "Example: To use this tool, you could provide the following JSON object as a tool call:"
|
|
26
47
|
|
|
48
|
+
|
|
27
49
|
def __init__(self):
|
|
28
50
|
self._formatting_registry = ToolFormattingRegistry()
|
|
29
51
|
logger.debug("ToolManifestProvider initialized.")
|
|
@@ -45,33 +67,36 @@ class ToolManifestProvider:
|
|
|
45
67
|
"""
|
|
46
68
|
tool_blocks = []
|
|
47
69
|
|
|
48
|
-
# Get the correct formatting pair from the registry, passing the override flag.
|
|
49
70
|
formatter_pair = self._formatting_registry.get_formatter_pair(provider, use_xml_tool_format=use_xml_tool_format)
|
|
50
71
|
schema_formatter = formatter_pair.schema_formatter
|
|
51
72
|
example_formatter = formatter_pair.example_formatter
|
|
52
73
|
|
|
53
|
-
# Determine if the chosen formatter is XML-based. This determines the final assembly format.
|
|
54
74
|
is_xml_format = isinstance(schema_formatter, DefaultXmlSchemaFormatter)
|
|
55
75
|
|
|
56
76
|
for td in tool_definitions:
|
|
57
77
|
try:
|
|
58
78
|
schema = schema_formatter.provide(td)
|
|
59
|
-
example = example_formatter.provide(td)
|
|
79
|
+
example = example_formatter.provide(td) # This is now a pre-formatted string for both XML and JSON
|
|
60
80
|
|
|
61
81
|
if schema and example:
|
|
62
82
|
if is_xml_format:
|
|
63
|
-
tool_blocks.append(f"{self.
|
|
64
|
-
else:
|
|
65
|
-
#
|
|
83
|
+
tool_blocks.append(f"{self.XML_SCHEMA_HEADER}\n{schema}\n\n{self.XML_EXAMPLE_HEADER}\n{example}")
|
|
84
|
+
else:
|
|
85
|
+
# For JSON, the schema is a dict, but the example is now a pre-formatted string.
|
|
66
86
|
schema_str = json.dumps(schema, indent=2)
|
|
67
|
-
|
|
68
|
-
tool_blocks.append(f"{self.
|
|
87
|
+
# FIX: Do NOT call json.dumps() on the 'example' variable, as it is already a string.
|
|
88
|
+
tool_blocks.append(f"{self.JSON_SCHEMA_HEADER}\n{schema_str}\n\n{self.JSON_EXAMPLE_HEADER}\n{example}")
|
|
69
89
|
else:
|
|
70
90
|
logger.warning(f"Could not generate schema or example for tool '{td.name}' using format {'XML' if is_xml_format else 'JSON'}.")
|
|
71
91
|
|
|
72
92
|
except Exception as e:
|
|
73
93
|
logger.error(f"Failed to generate manifest block for tool '{td.name}': {e}", exc_info=True)
|
|
74
94
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
# Assemble the final manifest string
|
|
96
|
+
manifest_content = "\n\n---\n\n".join(tool_blocks)
|
|
97
|
+
|
|
98
|
+
if is_xml_format and manifest_content:
|
|
99
|
+
# Prepend the general guidelines for XML format
|
|
100
|
+
return f"{self.XML_GENERAL_GUIDELINES}\n\n{self.XML_ARRAY_GUIDELINES}\n\n---\n\n{manifest_content}"
|
|
101
|
+
|
|
102
|
+
return manifest_content
|