autobyteus 1.1.5__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_config.py +6 -1
- autobyteus/agent/context/agent_runtime_state.py +7 -1
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +30 -7
- autobyteus/agent/handlers/tool_result_event_handler.py +100 -88
- autobyteus/agent/handlers/user_input_message_event_handler.py +22 -25
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +7 -1
- autobyteus/agent/message/__init__.py +7 -5
- autobyteus/agent/message/agent_input_user_message.py +6 -16
- autobyteus/agent/message/context_file.py +24 -24
- autobyteus/agent/message/context_file_type.py +29 -8
- autobyteus/agent/message/multimodal_message_builder.py +47 -0
- autobyteus/agent/streaming/stream_event_payloads.py +23 -4
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +6 -2
- autobyteus/agent/tool_invocation.py +27 -2
- autobyteus/agent_team/agent_team_builder.py +22 -1
- autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +9 -2
- autobyteus/agent_team/context/agent_team_config.py +1 -0
- autobyteus/agent_team/context/agent_team_runtime_state.py +0 -2
- autobyteus/llm/api/autobyteus_llm.py +33 -33
- autobyteus/llm/api/bedrock_llm.py +13 -5
- autobyteus/llm/api/claude_llm.py +13 -27
- autobyteus/llm/api/gemini_llm.py +108 -42
- autobyteus/llm/api/groq_llm.py +4 -3
- autobyteus/llm/api/mistral_llm.py +97 -51
- autobyteus/llm/api/nvidia_llm.py +6 -5
- autobyteus/llm/api/ollama_llm.py +37 -12
- autobyteus/llm/api/openai_compatible_llm.py +91 -91
- autobyteus/llm/autobyteus_provider.py +1 -1
- autobyteus/llm/base_llm.py +42 -139
- autobyteus/llm/extensions/base_extension.py +6 -6
- autobyteus/llm/extensions/token_usage_tracking_extension.py +3 -2
- autobyteus/llm/llm_factory.py +131 -61
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -1
- autobyteus/llm/user_message.py +43 -35
- autobyteus/llm/utils/llm_config.py +34 -18
- autobyteus/llm/utils/media_payload_formatter.py +99 -0
- autobyteus/llm/utils/messages.py +32 -25
- autobyteus/llm/utils/response_types.py +9 -3
- autobyteus/llm/utils/token_usage.py +6 -5
- autobyteus/multimedia/__init__.py +31 -0
- autobyteus/multimedia/audio/__init__.py +11 -0
- autobyteus/multimedia/audio/api/__init__.py +4 -0
- autobyteus/multimedia/audio/api/autobyteus_audio_client.py +59 -0
- autobyteus/multimedia/audio/api/gemini_audio_client.py +219 -0
- autobyteus/multimedia/audio/audio_client_factory.py +120 -0
- autobyteus/multimedia/audio/audio_model.py +97 -0
- autobyteus/multimedia/audio/autobyteus_audio_provider.py +108 -0
- autobyteus/multimedia/audio/base_audio_client.py +40 -0
- autobyteus/multimedia/image/__init__.py +11 -0
- autobyteus/multimedia/image/api/__init__.py +9 -0
- autobyteus/multimedia/image/api/autobyteus_image_client.py +97 -0
- autobyteus/multimedia/image/api/gemini_image_client.py +188 -0
- autobyteus/multimedia/image/api/openai_image_client.py +142 -0
- autobyteus/multimedia/image/autobyteus_image_provider.py +109 -0
- autobyteus/multimedia/image/base_image_client.py +67 -0
- autobyteus/multimedia/image/image_client_factory.py +118 -0
- autobyteus/multimedia/image/image_model.py +97 -0
- autobyteus/multimedia/providers.py +5 -0
- autobyteus/multimedia/runtimes.py +8 -0
- autobyteus/multimedia/utils/__init__.py +10 -0
- autobyteus/multimedia/utils/api_utils.py +19 -0
- autobyteus/multimedia/utils/multimedia_config.py +29 -0
- autobyteus/multimedia/utils/response_types.py +13 -0
- autobyteus/task_management/tools/publish_task_plan.py +4 -16
- autobyteus/task_management/tools/update_task_status.py +4 -19
- autobyteus/tools/__init__.py +5 -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/__init__.py +8 -0
- autobyteus/tools/multimedia/audio_tools.py +116 -0
- autobyteus/tools/multimedia/image_tools.py +186 -0
- autobyteus/tools/parameter_schema.py +82 -89
- autobyteus/tools/pydantic_schema_converter.py +81 -0
- autobyteus/tools/tool_category.py +1 -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/parsers/provider_aware_tool_usage_parser.py +5 -2
- autobyteus/tools/usage/providers/tool_manifest_provider.py +43 -16
- autobyteus/tools/usage/registries/tool_formatting_registry.py +9 -2
- autobyteus/tools/usage/registries/tool_usage_parser_registry.py +9 -2
- autobyteus-1.1.7.dist-info/METADATA +204 -0
- {autobyteus-1.1.5.dist-info → autobyteus-1.1.7.dist-info}/RECORD +98 -71
- examples/run_browser_agent.py +1 -1
- 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/llm/utils/image_payload_formatter.py +0 -89
- 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.5.dist-info/METADATA +0 -161
- {autobyteus-1.1.5.dist-info → autobyteus-1.1.7.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.5.dist-info → autobyteus-1.1.7.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.5.dist-info → autobyteus-1.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/tools/usage/formatters/google_json_example_formatter.py
|
|
2
|
-
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Any, TYPE_CHECKING, Optional
|
|
3
4
|
|
|
4
|
-
from autobyteus.tools.parameter_schema import
|
|
5
|
+
from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition
|
|
5
6
|
from .base_formatter import BaseExampleFormatter
|
|
6
7
|
# Import for reuse of the intelligent example generation logic
|
|
7
8
|
from .default_json_example_formatter import DefaultJsonExampleFormatter
|
|
@@ -10,32 +11,64 @@ if TYPE_CHECKING:
|
|
|
10
11
|
from autobyteus.tools.registry import ToolDefinition
|
|
11
12
|
|
|
12
13
|
class GoogleJsonExampleFormatter(BaseExampleFormatter):
|
|
13
|
-
"""
|
|
14
|
+
"""
|
|
15
|
+
Formats a tool usage example into the Google JSON tool_calls format.
|
|
16
|
+
Provides both basic (required only) and advanced (all) examples if optional
|
|
17
|
+
parameters exist for the tool.
|
|
18
|
+
"""
|
|
14
19
|
|
|
15
|
-
def provide(self, tool_definition: 'ToolDefinition') ->
|
|
20
|
+
def provide(self, tool_definition: 'ToolDefinition') -> str:
|
|
21
|
+
"""
|
|
22
|
+
Generates a formatted string containing basic and optionally an advanced usage example for the tool.
|
|
23
|
+
"""
|
|
24
|
+
basic_example_dict = self._create_example_structure(tool_definition, mode='basic')
|
|
25
|
+
basic_example_str = "### Example 1: Basic Call (Required Arguments)\n"
|
|
26
|
+
basic_example_str += "```json\n"
|
|
27
|
+
basic_example_str += json.dumps(basic_example_dict, indent=2)
|
|
28
|
+
basic_example_str += "\n```"
|
|
29
|
+
|
|
30
|
+
if not self._schema_has_advanced_params(tool_definition.argument_schema):
|
|
31
|
+
return basic_example_str
|
|
32
|
+
|
|
33
|
+
advanced_example_dict = self._create_example_structure(tool_definition, mode='advanced')
|
|
34
|
+
advanced_example_str = "### Example 2: Advanced Call (With Optional Arguments)\n"
|
|
35
|
+
advanced_example_str += "```json\n"
|
|
36
|
+
advanced_example_str += json.dumps(advanced_example_dict, indent=2)
|
|
37
|
+
advanced_example_str += "\n```"
|
|
38
|
+
|
|
39
|
+
return f"{basic_example_str}\n\n{advanced_example_str}"
|
|
40
|
+
|
|
41
|
+
def _create_example_structure(self, tool_definition: 'ToolDefinition', mode: str) -> Dict:
|
|
42
|
+
"""Helper to create a single Google tool call example for a given mode."""
|
|
16
43
|
tool_name = tool_definition.name
|
|
17
44
|
arg_schema = tool_definition.argument_schema
|
|
18
45
|
arguments = {}
|
|
19
46
|
|
|
20
47
|
if arg_schema and arg_schema.parameters:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
params_to_render = arg_schema.parameters
|
|
49
|
+
if mode == 'basic':
|
|
50
|
+
params_to_render = [p for p in arg_schema.parameters if p.required]
|
|
51
|
+
|
|
52
|
+
for param_def in params_to_render:
|
|
53
|
+
# Use the intelligent placeholder generator from the default formatter
|
|
54
|
+
arguments[param_def.name] = DefaultJsonExampleFormatter._generate_example_from_schema(
|
|
55
|
+
param_def.object_schema or param_def.array_item_schema or param_def.param_type,
|
|
56
|
+
param_def.object_schema or arg_schema,
|
|
57
|
+
mode=mode
|
|
58
|
+
) if param_def.object_schema or param_def.array_item_schema else self._generate_simple_placeholder(param_def)
|
|
59
|
+
|
|
25
60
|
return {"name": tool_name, "args": arguments}
|
|
26
61
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
if
|
|
30
|
-
|
|
62
|
+
def _schema_has_advanced_params(self, schema: Optional[ParameterSchema]) -> bool:
|
|
63
|
+
"""Recursively checks if a schema or any of its sub-schemas have non-required parameters."""
|
|
64
|
+
if not schema: return False
|
|
65
|
+
for param in schema.parameters:
|
|
66
|
+
if not param.required: return True
|
|
67
|
+
if param.object_schema and self._schema_has_advanced_params(param.object_schema): return True
|
|
68
|
+
if isinstance(param.array_item_schema, ParameterSchema) and self._schema_has_advanced_params(param.array_item_schema): return True
|
|
69
|
+
return False
|
|
31
70
|
|
|
32
|
-
|
|
71
|
+
def _generate_simple_placeholder(self, param_def: ParameterDefinition) -> Any:
|
|
72
|
+
"""Generates a simple placeholder for primitive types."""
|
|
33
73
|
if param_def.default_value is not None: return param_def.default_value
|
|
34
|
-
|
|
35
|
-
if param_def.param_type == ParameterType.INTEGER: return 123
|
|
36
|
-
if param_def.param_type == ParameterType.FLOAT: return 123.45
|
|
37
|
-
if param_def.param_type == ParameterType.BOOLEAN: return True
|
|
38
|
-
if param_def.param_type == ParameterType.ENUM: return param_def.enum_values[0] if param_def.enum_values else "enum_val"
|
|
39
|
-
if param_def.param_type == ParameterType.OBJECT: return {"key": "value"}
|
|
40
|
-
if param_def.param_type == ParameterType.ARRAY: return ["item1", "item2"]
|
|
41
|
-
return "placeholder"
|
|
74
|
+
return DefaultJsonExampleFormatter._generate_example_from_schema(param_def.param_type, param_def.param_type, mode='basic')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/tools/usage/formatters/openai_json_example_formatter.py
|
|
2
2
|
import json
|
|
3
|
-
from typing import Dict, Any, TYPE_CHECKING
|
|
3
|
+
from typing import Dict, Any, TYPE_CHECKING, Optional
|
|
4
4
|
|
|
5
|
-
from autobyteus.tools.parameter_schema import
|
|
5
|
+
from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition
|
|
6
6
|
from .base_formatter import BaseExampleFormatter
|
|
7
7
|
from .default_json_example_formatter import DefaultJsonExampleFormatter # Import for reuse
|
|
8
8
|
|
|
@@ -11,41 +11,71 @@ if TYPE_CHECKING:
|
|
|
11
11
|
|
|
12
12
|
class OpenAiJsonExampleFormatter(BaseExampleFormatter):
|
|
13
13
|
"""
|
|
14
|
-
Formats a tool usage example into
|
|
15
|
-
|
|
14
|
+
Formats a tool usage example into the OpenAI JSON 'tool_calls' format.
|
|
15
|
+
Provides both basic (required only) and advanced (all) examples if optional
|
|
16
|
+
parameters exist for the tool.
|
|
16
17
|
"""
|
|
18
|
+
|
|
19
|
+
def provide(self, tool_definition: 'ToolDefinition') -> str:
|
|
20
|
+
"""
|
|
21
|
+
Generates a formatted string containing basic and optionally an advanced usage example for the tool.
|
|
22
|
+
"""
|
|
23
|
+
basic_example_dict = self._create_example_structure(tool_definition, mode='basic')
|
|
24
|
+
basic_example_str = "### Example 1: Basic Call (Required Arguments)\n"
|
|
25
|
+
basic_example_str += "```json\n"
|
|
26
|
+
basic_example_str += json.dumps(basic_example_dict, indent=2)
|
|
27
|
+
basic_example_str += "\n```"
|
|
28
|
+
|
|
29
|
+
if not self._schema_has_advanced_params(tool_definition.argument_schema):
|
|
30
|
+
return basic_example_str
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
advanced_example_dict = self._create_example_structure(tool_definition, mode='advanced')
|
|
33
|
+
advanced_example_str = "### Example 2: Advanced Call (With Optional Arguments)\n"
|
|
34
|
+
advanced_example_str += "```json\n"
|
|
35
|
+
advanced_example_str += json.dumps(advanced_example_dict, indent=2)
|
|
36
|
+
advanced_example_str += "\n```"
|
|
37
|
+
|
|
38
|
+
return f"{basic_example_str}\n\n{advanced_example_str}"
|
|
39
|
+
|
|
40
|
+
def _create_example_structure(self, tool_definition: 'ToolDefinition', mode: str) -> Dict:
|
|
41
|
+
"""Helper to create a single OpenAI tool call example for a given mode."""
|
|
19
42
|
tool_name = tool_definition.name
|
|
20
43
|
arg_schema = tool_definition.argument_schema
|
|
21
44
|
arguments = {}
|
|
22
45
|
|
|
23
46
|
if arg_schema and arg_schema.parameters:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
params_to_render = arg_schema.parameters
|
|
48
|
+
if mode == 'basic':
|
|
49
|
+
params_to_render = [p for p in arg_schema.parameters if p.required]
|
|
50
|
+
|
|
51
|
+
for param_def in params_to_render:
|
|
52
|
+
# Use the intelligent placeholder generator from the default formatter
|
|
53
|
+
arguments[param_def.name] = DefaultJsonExampleFormatter._generate_example_from_schema(
|
|
54
|
+
param_def.object_schema or param_def.array_item_schema or param_def.param_type,
|
|
55
|
+
param_def.object_schema or arg_schema,
|
|
56
|
+
mode=mode
|
|
57
|
+
) if param_def.object_schema or param_def.array_item_schema else self._generate_simple_placeholder(param_def)
|
|
27
58
|
|
|
28
59
|
function_call = {
|
|
29
60
|
"function": {
|
|
30
61
|
"name": tool_name,
|
|
31
|
-
|
|
62
|
+
# FIX: Keep arguments as a dictionary for clear examples in the prompt.
|
|
63
|
+
# Do NOT stringify it here.
|
|
64
|
+
"arguments": arguments,
|
|
32
65
|
},
|
|
33
66
|
}
|
|
34
|
-
|
|
35
67
|
return {"tool": function_call}
|
|
36
68
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
if
|
|
40
|
-
|
|
69
|
+
def _schema_has_advanced_params(self, schema: Optional[ParameterSchema]) -> bool:
|
|
70
|
+
"""Recursively checks if a schema or any of its sub-schemas have non-required parameters."""
|
|
71
|
+
if not schema: return False
|
|
72
|
+
for param in schema.parameters:
|
|
73
|
+
if not param.required: return True
|
|
74
|
+
if param.object_schema and self._schema_has_advanced_params(param.object_schema): return True
|
|
75
|
+
if isinstance(param.array_item_schema, ParameterSchema) and self._schema_has_advanced_params(param.array_item_schema): return True
|
|
76
|
+
return False
|
|
41
77
|
|
|
42
|
-
|
|
78
|
+
def _generate_simple_placeholder(self, param_def: ParameterDefinition) -> Any:
|
|
79
|
+
"""Generates a simple placeholder for primitive types."""
|
|
43
80
|
if param_def.default_value is not None: return param_def.default_value
|
|
44
|
-
|
|
45
|
-
if param_def.param_type == ParameterType.INTEGER: return 123
|
|
46
|
-
if param_def.param_type == ParameterType.FLOAT: return 123.45
|
|
47
|
-
if param_def.param_type == ParameterType.BOOLEAN: return True
|
|
48
|
-
if param_def.param_type == ParameterType.ENUM: return param_def.enum_values[0] if param_def.enum_values else "enum_val"
|
|
49
|
-
if param_def.param_type == ParameterType.OBJECT: return {"key": "value"}
|
|
50
|
-
if param_def.param_type == ParameterType.ARRAY: return ["item1", "item2"]
|
|
51
|
-
return "placeholder"
|
|
81
|
+
return DefaultJsonExampleFormatter._generate_example_from_schema(param_def.param_type, param_def.param_type, mode='basic')
|
|
@@ -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
|
+
|
|
@@ -44,8 +44,11 @@ class ProviderAwareToolUsageParser:
|
|
|
44
44
|
else:
|
|
45
45
|
logger.warning(f"Agent '{context.agent_id}': LLM instance or model not available. Cannot determine provider for tool response parsing.")
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
|
|
47
|
+
# Retrieve the override flag from the agent's configuration.
|
|
48
|
+
use_xml_tool_format = context.config.use_xml_tool_format
|
|
49
|
+
|
|
50
|
+
# Get the correct parser from the registry, passing the override flag.
|
|
51
|
+
parser = self._parser_registry.get_parser(llm_provider, use_xml_tool_format=use_xml_tool_format)
|
|
49
52
|
|
|
50
53
|
logger.debug(f"ProviderAwareToolUsageParser selected delegate parser '{parser.get_name()}' for LLM provider '{llm_provider.name if llm_provider else 'Unknown'}'.")
|
|
51
54
|
|