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.
Files changed (43) hide show
  1. autobyteus/agent/context/agent_runtime_state.py +7 -1
  2. autobyteus/agent/handlers/tool_result_event_handler.py +100 -88
  3. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +7 -1
  4. autobyteus/agent/tool_invocation.py +25 -1
  5. autobyteus/agent_team/agent_team_builder.py +22 -1
  6. autobyteus/agent_team/context/agent_team_runtime_state.py +0 -2
  7. autobyteus/llm/llm_factory.py +25 -57
  8. autobyteus/llm/ollama_provider_resolver.py +1 -0
  9. autobyteus/llm/providers.py +1 -0
  10. autobyteus/llm/token_counter/token_counter_factory.py +2 -0
  11. autobyteus/multimedia/audio/audio_model.py +2 -1
  12. autobyteus/multimedia/image/image_model.py +2 -1
  13. autobyteus/task_management/tools/publish_task_plan.py +4 -16
  14. autobyteus/task_management/tools/update_task_status.py +4 -19
  15. autobyteus/tools/__init__.py +2 -4
  16. autobyteus/tools/base_tool.py +98 -29
  17. autobyteus/tools/browser/standalone/__init__.py +0 -1
  18. autobyteus/tools/google_search.py +149 -0
  19. autobyteus/tools/mcp/schema_mapper.py +29 -71
  20. autobyteus/tools/multimedia/audio_tools.py +3 -3
  21. autobyteus/tools/multimedia/image_tools.py +5 -5
  22. autobyteus/tools/parameter_schema.py +82 -89
  23. autobyteus/tools/pydantic_schema_converter.py +81 -0
  24. autobyteus/tools/usage/formatters/default_json_example_formatter.py +89 -20
  25. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +115 -41
  26. autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +50 -20
  27. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +55 -22
  28. autobyteus/tools/usage/formatters/google_json_example_formatter.py +54 -21
  29. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +53 -23
  30. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +270 -94
  31. autobyteus/tools/usage/providers/tool_manifest_provider.py +39 -14
  32. autobyteus-1.1.7.dist-info/METADATA +204 -0
  33. {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/RECORD +39 -40
  34. examples/run_google_slides_agent.py +2 -2
  35. examples/run_mcp_google_slides_client.py +1 -1
  36. examples/run_sqlite_agent.py +1 -1
  37. autobyteus/tools/ask_user_input.py +0 -40
  38. autobyteus/tools/browser/standalone/factory/google_search_factory.py +0 -25
  39. autobyteus/tools/browser/standalone/google_search_ui.py +0 -126
  40. autobyteus-1.1.6.dist-info/METADATA +0 -161
  41. {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/WHEEL +0 -0
  42. {autobyteus-1.1.6.dist-info → autobyteus-1.1.7.dist-info}/licenses/LICENSE +0 -0
  43. {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 using a robust,
22
- stateful, character-by-character scanning approach. This parser can correctly
23
- identify and extract valid <tool>...</tool> blocks even when they are mixed with
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
- cursor = 0
33
-
34
- while cursor < len(text):
35
- # Find the start of the next potential tool tag from the current cursor position
36
- tool_start_index = text.find('<tool', cursor)
37
- if tool_start_index == -1:
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
- # Check if another '<' appears before the '>', which would indicate a malformed/aborted tag.
47
- # Example: <tool name="abc" ... <tool name="xyz">
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
- # Find the corresponding </tool> closing tag
56
- tool_end_index = text.find('</tool>', tool_start_tag_end)
57
- if tool_end_index == -1:
58
- # Found a start tag but no end tag, treat as fragment and advance
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
- # Extract the full content of this potential tool block
63
- block_end_pos = tool_end_index + len('</tool>')
64
- tool_block = text[tool_start_index:block_end_pos]
252
+ cursor = open_tag_end + 1
253
+ nesting_level = 1
254
+ content_end = -1
65
255
 
66
- # CRITICAL NESTING CHECK:
67
- # Check if there is another '<tool' start tag within this block.
68
- # If so, it means this is a malformed, nested block. We must skip it
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
- # This is a valid, non-nested block. Attempt to parse it.
77
- try:
78
- # Preprocessing and parsing
79
- processed_block = self._preprocess_xml_for_parsing(tool_block)
80
- root = ET.fromstring(processed_block)
81
-
82
- tool_name = root.attrib.get("name")
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
- arguments = self._parse_arguments_from_xml(root)
87
- tool_id_attr = root.attrib.get('id')
88
-
89
- invocation = ToolInvocation(
90
- name=tool_name,
91
- arguments=arguments,
92
- id=tool_id_attr
93
- )
94
- invocations.append(invocation)
95
- logger.info(f"Successfully parsed XML tool invocation for '{tool_name}'.")
96
-
97
- except (ET.ParseError, xml.parsers.expat.ExpatError) as e:
98
- # The self-contained block was still malformed. Log and ignore it.
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
- # CRITICAL: Advance cursor past the entire block we just processed
102
- cursor = block_end_pos
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 _preprocess_xml_for_parsing(self, xml_content: str) -> str:
107
- # This function remains the same as it's not part of the core logic error.
108
- # It's a helper for cleaning up minor syntax issues before parsing.
109
- return xml_content
110
-
111
- def _parse_arguments_from_xml(self, command_element: ET.Element) -> Dict[str, Any]:
112
- """Helper to extract arguments from a parsed <tool> element."""
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
- SCHEMA_HEADER = "## Tool Definition:"
23
- EXAMPLE_HEADER = "## Example Usage:"
24
- # UPDATED: Changed the header to be more descriptive as requested.
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.SCHEMA_HEADER}\n{schema}\n\n{self.EXAMPLE_HEADER}\n{example}")
64
- else: # JSON format
65
- # UPDATED: Removed the redundant {"tool": schema} wrapper.
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
- example_str = json.dumps(example, indent=2)
68
- tool_blocks.append(f"{self.SCHEMA_HEADER}\n{schema_str}\n\n{self.JSON_EXAMPLE_HEADER}\n{example_str}")
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
- # UPDATED: Unify the return for all formats to provide a consistent structure
76
- # without the incorrect '[]' wrapper for JSON.
77
- return "\n\n---\n\n".join(tool_blocks)
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