autobyteus 1.0.5__py3-none-any.whl → 1.1.0__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/agent.py +97 -222
- autobyteus/agent/bootstrap_steps/__init__.py +19 -0
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +88 -0
- autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +57 -0
- autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +38 -0
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +93 -0
- autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +47 -0
- autobyteus/agent/context/__init__.py +18 -0
- autobyteus/agent/context/agent_config.py +80 -0
- autobyteus/agent/context/agent_context.py +132 -0
- autobyteus/agent/context/agent_phase_manager.py +164 -0
- autobyteus/agent/context/agent_runtime_state.py +89 -0
- autobyteus/agent/context/phases.py +47 -0
- autobyteus/agent/events/__init__.py +63 -0
- autobyteus/agent/events/agent_events.py +147 -0
- autobyteus/agent/events/agent_input_event_queue_manager.py +174 -0
- autobyteus/agent/events/notifiers.py +104 -0
- autobyteus/agent/events/worker_event_dispatcher.py +118 -0
- autobyteus/agent/factory/__init__.py +9 -0
- autobyteus/agent/factory/agent_factory.py +126 -79
- autobyteus/agent/group/agent_group.py +155 -0
- autobyteus/agent/group/agent_group_context.py +81 -0
- autobyteus/agent/handlers/__init__.py +36 -0
- autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +134 -0
- autobyteus/agent/handlers/base_event_handler.py +36 -0
- autobyteus/agent/handlers/event_handler_registry.py +76 -0
- autobyteus/agent/handlers/generic_event_handler.py +46 -0
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +76 -0
- autobyteus/agent/handlers/lifecycle_event_logger.py +64 -0
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +136 -0
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +140 -0
- autobyteus/agent/handlers/tool_execution_approval_event_handler.py +85 -0
- autobyteus/agent/handlers/tool_invocation_request_event_handler.py +186 -0
- autobyteus/agent/handlers/tool_result_event_handler.py +96 -0
- autobyteus/agent/handlers/user_input_message_event_handler.py +77 -0
- autobyteus/agent/hooks/__init__.py +9 -0
- autobyteus/agent/hooks/base_phase_hook.py +52 -0
- autobyteus/agent/input_processor/__init__.py +18 -0
- autobyteus/agent/input_processor/base_user_input_processor.py +51 -0
- autobyteus/agent/input_processor/content_prefixing_input_processor.py +41 -0
- autobyteus/agent/input_processor/metadata_appending_input_processor.py +34 -0
- autobyteus/agent/input_processor/passthrough_input_processor.py +32 -0
- autobyteus/agent/input_processor/processor_definition.py +42 -0
- autobyteus/agent/input_processor/processor_meta.py +46 -0
- autobyteus/agent/input_processor/processor_registry.py +98 -0
- autobyteus/agent/llm_response_processor/__init__.py +16 -0
- autobyteus/agent/llm_response_processor/base_processor.py +50 -0
- autobyteus/agent/llm_response_processor/processor_definition.py +36 -0
- autobyteus/agent/llm_response_processor/processor_meta.py +37 -0
- autobyteus/agent/llm_response_processor/processor_registry.py +94 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +53 -0
- autobyteus/agent/message/__init__.py +20 -0
- autobyteus/agent/message/agent_input_user_message.py +96 -0
- autobyteus/agent/message/context_file.py +82 -0
- autobyteus/agent/message/context_file_type.py +64 -0
- autobyteus/agent/message/{message.py → inter_agent_message.py} +12 -12
- autobyteus/agent/message/{message_types.py → inter_agent_message_type.py} +8 -6
- autobyteus/agent/message/send_message_to.py +142 -36
- autobyteus/agent/remote_agent.py +240 -5
- autobyteus/agent/runtime/__init__.py +15 -0
- autobyteus/agent/runtime/agent_runtime.py +139 -0
- autobyteus/agent/runtime/agent_thread_pool_manager.py +88 -0
- autobyteus/agent/runtime/agent_worker.py +200 -0
- autobyteus/agent/streaming/__init__.py +15 -0
- autobyteus/agent/streaming/agent_event_stream.py +120 -0
- autobyteus/agent/streaming/queue_streamer.py +58 -0
- autobyteus/agent/streaming/stream_event_payloads.py +156 -0
- autobyteus/agent/streaming/stream_events.py +123 -0
- autobyteus/agent/system_prompt_processor/__init__.py +14 -0
- autobyteus/agent/system_prompt_processor/base_processor.py +45 -0
- autobyteus/agent/system_prompt_processor/processor_definition.py +40 -0
- autobyteus/agent/system_prompt_processor/processor_meta.py +47 -0
- autobyteus/agent/system_prompt_processor/processor_registry.py +119 -0
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +65 -0
- autobyteus/agent/tool_invocation.py +28 -5
- autobyteus/agent/utils/__init__.py +9 -0
- autobyteus/agent/utils/wait_for_idle.py +59 -0
- autobyteus/agent/workflow/__init__.py +11 -0
- autobyteus/agent/workflow/agentic_workflow.py +89 -0
- autobyteus/agent/workflow/base_agentic_workflow.py +98 -0
- autobyteus/agent/workspace/__init__.py +9 -0
- autobyteus/agent/workspace/base_workspace.py +55 -0
- autobyteus/cli/__init__.py +10 -0
- autobyteus/cli/agent_cli.py +299 -0
- autobyteus/events/event_emitter.py +33 -56
- autobyteus/events/event_manager.py +133 -66
- autobyteus/events/event_types.py +41 -14
- autobyteus/llm/api/autobyteus_llm.py +13 -15
- autobyteus/llm/api/bedrock_llm.py +9 -3
- autobyteus/llm/api/claude_llm.py +10 -5
- autobyteus/llm/api/deepseek_llm.py +53 -91
- autobyteus/llm/api/gemini_llm.py +10 -4
- autobyteus/llm/api/grok_llm.py +53 -77
- autobyteus/llm/api/groq_llm.py +10 -5
- autobyteus/llm/api/mistral_llm.py +10 -5
- autobyteus/llm/api/nvidia_llm.py +9 -4
- autobyteus/llm/api/ollama_llm.py +56 -48
- autobyteus/llm/api/openai_llm.py +20 -14
- autobyteus/llm/base_llm.py +95 -34
- autobyteus/llm/extensions/base_extension.py +3 -4
- autobyteus/llm/extensions/token_usage_tracking_extension.py +2 -3
- autobyteus/llm/llm_factory.py +12 -13
- autobyteus/llm/models.py +87 -8
- autobyteus/llm/user_message.py +73 -0
- autobyteus/llm/utils/llm_config.py +124 -27
- autobyteus/llm/utils/response_types.py +3 -2
- autobyteus/llm/utils/token_usage.py +7 -4
- autobyteus/rpc/__init__.py +73 -0
- autobyteus/rpc/client/__init__.py +17 -0
- autobyteus/rpc/client/abstract_client_connection.py +124 -0
- autobyteus/rpc/client/client_connection_manager.py +153 -0
- autobyteus/rpc/client/sse_client_connection.py +306 -0
- autobyteus/rpc/client/stdio_client_connection.py +280 -0
- autobyteus/rpc/config/__init__.py +13 -0
- autobyteus/rpc/config/agent_server_config.py +153 -0
- autobyteus/rpc/config/agent_server_registry.py +152 -0
- autobyteus/rpc/hosting.py +244 -0
- autobyteus/rpc/protocol.py +244 -0
- autobyteus/rpc/server/__init__.py +20 -0
- autobyteus/rpc/server/agent_server_endpoint.py +181 -0
- autobyteus/rpc/server/base_method_handler.py +40 -0
- autobyteus/rpc/server/method_handlers.py +259 -0
- autobyteus/rpc/server/sse_server_handler.py +182 -0
- autobyteus/rpc/server/stdio_server_handler.py +151 -0
- autobyteus/rpc/server_main.py +198 -0
- autobyteus/rpc/transport_type.py +13 -0
- autobyteus/tools/__init__.py +75 -0
- autobyteus/tools/ask_user_input.py +34 -77
- autobyteus/tools/base_tool.py +66 -37
- autobyteus/tools/bash/__init__.py +2 -0
- autobyteus/tools/bash/bash_executor.py +42 -79
- autobyteus/tools/browser/__init__.py +2 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +50 -42
- autobyteus/tools/browser/session_aware/browser_session_aware_tool.py +7 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +117 -125
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +75 -22
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +94 -28
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_web_element_trigger_factory.py +10 -2
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_reader_factory.py +18 -2
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_screenshot_taker_factory.py +10 -2
- autobyteus/tools/browser/session_aware/shared_browser_session_manager.py +4 -3
- autobyteus/tools/browser/standalone/__init__.py +7 -0
- autobyteus/tools/browser/standalone/factory/google_search_factory.py +17 -2
- autobyteus/tools/browser/standalone/factory/webpage_reader_factory.py +17 -2
- autobyteus/tools/browser/standalone/factory/webpage_screenshot_taker_factory.py +10 -2
- autobyteus/tools/browser/standalone/google_search_ui.py +104 -67
- autobyteus/tools/browser/standalone/navigate_to.py +52 -28
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +94 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +146 -61
- autobyteus/tools/browser/standalone/webpage_reader.py +80 -61
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +91 -45
- autobyteus/tools/factory/__init__.py +9 -0
- autobyteus/tools/factory/tool_factory.py +25 -4
- autobyteus/tools/file/file_reader.py +22 -51
- autobyteus/tools/file/file_writer.py +25 -56
- autobyteus/tools/functional_tool.py +234 -0
- autobyteus/tools/image_downloader.py +49 -71
- autobyteus/tools/mcp/__init__.py +47 -0
- autobyteus/tools/mcp/call_handlers/__init__.py +18 -0
- autobyteus/tools/mcp/call_handlers/base_handler.py +40 -0
- autobyteus/tools/mcp/call_handlers/sse_handler.py +22 -0
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +62 -0
- autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +55 -0
- autobyteus/tools/mcp/config_service.py +258 -0
- autobyteus/tools/mcp/factory.py +70 -0
- autobyteus/tools/mcp/registrar.py +135 -0
- autobyteus/tools/mcp/schema_mapper.py +131 -0
- autobyteus/tools/mcp/tool.py +101 -0
- autobyteus/tools/mcp/types.py +96 -0
- autobyteus/tools/parameter_schema.py +268 -0
- autobyteus/tools/pdf_downloader.py +78 -79
- autobyteus/tools/registry/__init__.py +0 -2
- autobyteus/tools/registry/tool_definition.py +106 -34
- autobyteus/tools/registry/tool_registry.py +46 -22
- autobyteus/tools/timer.py +150 -102
- autobyteus/tools/tool_config.py +117 -0
- autobyteus/tools/tool_meta.py +48 -26
- autobyteus/tools/usage/__init__.py +6 -0
- autobyteus/tools/usage/formatters/__init__.py +31 -0
- autobyteus/tools/usage/formatters/anthropic_json_example_formatter.py +18 -0
- autobyteus/tools/usage/formatters/anthropic_json_schema_formatter.py +25 -0
- autobyteus/tools/usage/formatters/base_formatter.py +42 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +42 -0
- autobyteus/tools/usage/formatters/default_json_schema_formatter.py +28 -0
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +55 -0
- autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +46 -0
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +34 -0
- autobyteus/tools/usage/formatters/gemini_json_schema_formatter.py +25 -0
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +34 -0
- autobyteus/tools/usage/formatters/google_json_schema_formatter.py +25 -0
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +49 -0
- autobyteus/tools/usage/formatters/openai_json_schema_formatter.py +28 -0
- autobyteus/tools/usage/parsers/__init__.py +22 -0
- autobyteus/tools/usage/parsers/anthropic_xml_tool_usage_parser.py +10 -0
- autobyteus/tools/usage/parsers/base_parser.py +41 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +106 -0
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +135 -0
- autobyteus/tools/usage/parsers/exceptions.py +13 -0
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +68 -0
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +147 -0
- autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +67 -0
- autobyteus/tools/usage/providers/__init__.py +22 -0
- autobyteus/tools/usage/providers/json_example_provider.py +32 -0
- autobyteus/tools/usage/providers/json_schema_provider.py +35 -0
- autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +28 -0
- autobyteus/tools/usage/providers/tool_manifest_provider.py +68 -0
- autobyteus/tools/usage/providers/xml_example_provider.py +28 -0
- autobyteus/tools/usage/providers/xml_schema_provider.py +29 -0
- autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +26 -0
- autobyteus/tools/usage/registries/__init__.py +20 -0
- autobyteus/tools/usage/registries/json_example_formatter_registry.py +51 -0
- autobyteus/tools/usage/registries/json_schema_formatter_registry.py +51 -0
- autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +42 -0
- autobyteus/tools/usage/registries/xml_example_formatter_registry.py +30 -0
- autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +33 -0
- autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +30 -0
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/METADATA +21 -3
- autobyteus-1.1.0.dist-info/RECORD +279 -0
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/WHEEL +1 -1
- autobyteus/agent/async_agent.py +0 -175
- autobyteus/agent/async_group_aware_agent.py +0 -136
- autobyteus/agent/group/async_group_aware_agent.py +0 -122
- autobyteus/agent/group/coordinator_agent.py +0 -36
- autobyteus/agent/group/group_aware_agent.py +0 -121
- autobyteus/agent/orchestrator/__init__.py +0 -0
- autobyteus/agent/orchestrator/base_agent_orchestrator.py +0 -82
- autobyteus/agent/orchestrator/multi_replica_agent_orchestrator.py +0 -72
- autobyteus/agent/orchestrator/single_replica_agent_orchestrator.py +0 -43
- autobyteus/agent/registry/__init__.py +0 -11
- autobyteus/agent/registry/agent_definition.py +0 -94
- autobyteus/agent/registry/agent_registry.py +0 -114
- autobyteus/agent/response_parser/__init__.py +0 -0
- autobyteus/agent/response_parser/tool_usage_command_parser.py +0 -100
- autobyteus/agent/status.py +0 -12
- autobyteus/conversation/__init__.py +0 -0
- autobyteus/conversation/conversation.py +0 -54
- autobyteus/conversation/user_message.py +0 -59
- autobyteus/events/decorators.py +0 -29
- autobyteus/prompt/prompt_version_manager.py +0 -58
- autobyteus/prompt/storage/__init__.py +0 -0
- autobyteus/prompt/storage/prompt_version_model.py +0 -29
- autobyteus/prompt/storage/prompt_version_repository.py +0 -83
- autobyteus/tools/bash/factory/__init__.py +0 -0
- autobyteus/tools/bash/factory/bash_executor_factory.py +0 -6
- autobyteus/tools/factory/ask_user_input_factory.py +0 -6
- autobyteus/tools/factory/image_downloader_factory.py +0 -9
- autobyteus/tools/factory/pdf_downloader_factory.py +0 -9
- autobyteus/tools/factory/webpage_image_downloader_factory.py +0 -6
- autobyteus/tools/file/factory/__init__.py +0 -0
- autobyteus/tools/file/factory/file_reader_factory.py +0 -6
- autobyteus/tools/file/factory/file_writer_factory.py +0 -6
- autobyteus/tools/mcp_remote_tool.py +0 -82
- autobyteus/tools/web_page_pdf_generator.py +0 -90
- autobyteus-1.0.5.dist-info/RECORD +0 -163
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/default_json_schema_formatter.py
|
|
2
|
+
from typing import Dict, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from .base_formatter import BaseSchemaFormatter
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
8
|
+
|
|
9
|
+
class DefaultJsonSchemaFormatter(BaseSchemaFormatter):
|
|
10
|
+
"""
|
|
11
|
+
Formats a tool's schema into a generic, provider-agnostic JSON format.
|
|
12
|
+
This serves as the default for JSON-based schema representation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
16
|
+
name = tool_definition.name
|
|
17
|
+
description = tool_definition.description
|
|
18
|
+
arg_schema = tool_definition.argument_schema
|
|
19
|
+
|
|
20
|
+
input_schema = arg_schema.to_json_schema_dict() if arg_schema else {
|
|
21
|
+
"type": "object", "properties": {}, "required": []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
"name": name,
|
|
26
|
+
"description": description,
|
|
27
|
+
"inputSchema": input_schema,
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/default_xml_example_formatter.py
|
|
2
|
+
import xml.sax.saxutils
|
|
3
|
+
from typing import Any, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from autobyteus.tools.parameter_schema import ParameterType, ParameterDefinition
|
|
6
|
+
from .base_formatter import BaseExampleFormatter
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
10
|
+
|
|
11
|
+
class DefaultXmlExampleFormatter(BaseExampleFormatter):
|
|
12
|
+
"""Formats a tool usage example into a standardized XML <tool> string."""
|
|
13
|
+
|
|
14
|
+
def provide(self, tool_definition: 'ToolDefinition') -> str:
|
|
15
|
+
tool_name = tool_definition.name
|
|
16
|
+
arg_schema = tool_definition.argument_schema
|
|
17
|
+
|
|
18
|
+
example_xml_parts = [f'<tool name="{tool_name}">']
|
|
19
|
+
arguments_part = []
|
|
20
|
+
|
|
21
|
+
if arg_schema and arg_schema.parameters:
|
|
22
|
+
for param_def in arg_schema.parameters:
|
|
23
|
+
if param_def.required or param_def.default_value is not None:
|
|
24
|
+
placeholder_value = self._generate_placeholder_value(param_def)
|
|
25
|
+
escaped_value = xml.sax.saxutils.escape(str(placeholder_value))
|
|
26
|
+
arguments_part.append(f' <arg name="{param_def.name}">{escaped_value}</arg>')
|
|
27
|
+
|
|
28
|
+
if arguments_part:
|
|
29
|
+
example_xml_parts.append(" <arguments>")
|
|
30
|
+
example_xml_parts.extend(arguments_part)
|
|
31
|
+
example_xml_parts.append(" </arguments>")
|
|
32
|
+
else:
|
|
33
|
+
example_xml_parts.append(" <!-- This tool takes no arguments -->")
|
|
34
|
+
|
|
35
|
+
example_xml_parts.append("</tool>")
|
|
36
|
+
return "\n".join(example_xml_parts)
|
|
37
|
+
|
|
38
|
+
def _generate_placeholder_value(self, param_def: ParameterDefinition) -> Any:
|
|
39
|
+
if param_def.default_value is not None:
|
|
40
|
+
return param_def.default_value
|
|
41
|
+
if param_def.param_type == ParameterType.STRING:
|
|
42
|
+
return f"example_{param_def.name}"
|
|
43
|
+
if param_def.param_type == ParameterType.INTEGER:
|
|
44
|
+
return 123
|
|
45
|
+
if param_def.param_type == ParameterType.FLOAT:
|
|
46
|
+
return 123.45
|
|
47
|
+
if param_def.param_type == ParameterType.BOOLEAN:
|
|
48
|
+
return True
|
|
49
|
+
if param_def.param_type == ParameterType.ENUM:
|
|
50
|
+
return param_def.enum_values[0] if param_def.enum_values else "enum_val"
|
|
51
|
+
if param_def.param_type == ParameterType.OBJECT:
|
|
52
|
+
return {"key": "value"}
|
|
53
|
+
if param_def.param_type == ParameterType.ARRAY:
|
|
54
|
+
return ["item1", "item2"]
|
|
55
|
+
return "placeholder"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/default_xml_schema_formatter.py
|
|
2
|
+
import xml.sax.saxutils
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from autobyteus.tools.parameter_schema import ParameterType
|
|
6
|
+
from .base_formatter import BaseSchemaFormatter
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
10
|
+
|
|
11
|
+
class DefaultXmlSchemaFormatter(BaseSchemaFormatter):
|
|
12
|
+
"""Formats a tool's schema into a standardized XML string."""
|
|
13
|
+
|
|
14
|
+
def provide(self, tool_definition: 'ToolDefinition') -> str:
|
|
15
|
+
arg_schema = tool_definition.argument_schema
|
|
16
|
+
tool_name = tool_definition.name
|
|
17
|
+
description = tool_definition.description
|
|
18
|
+
|
|
19
|
+
escaped_description = xml.sax.saxutils.escape(description) if description else ""
|
|
20
|
+
tool_tag = f'<tool name="{tool_name}" description="{escaped_description}">'
|
|
21
|
+
xml_parts = [tool_tag]
|
|
22
|
+
|
|
23
|
+
if arg_schema and arg_schema.parameters:
|
|
24
|
+
xml_parts.append(" <arguments>")
|
|
25
|
+
for param in arg_schema.parameters:
|
|
26
|
+
arg_tag = f' <arg name="{param.name}"'
|
|
27
|
+
arg_tag += f' type="{param.param_type.value}"'
|
|
28
|
+
if param.description:
|
|
29
|
+
escaped_param_desc = xml.sax.saxutils.escape(param.description)
|
|
30
|
+
arg_tag += f' description="{escaped_param_desc}"'
|
|
31
|
+
arg_tag += f" required=\"{'true' if param.required else 'false'}\""
|
|
32
|
+
|
|
33
|
+
if param.default_value is not None:
|
|
34
|
+
arg_tag += f' default="{xml.sax.saxutils.escape(str(param.default_value))}"'
|
|
35
|
+
if param.param_type == ParameterType.ENUM and param.enum_values:
|
|
36
|
+
escaped_enum_values = [xml.sax.saxutils.escape(ev) for ev in param.enum_values]
|
|
37
|
+
arg_tag += f' enum_values="{",".join(escaped_enum_values)}"'
|
|
38
|
+
|
|
39
|
+
arg_tag += " />"
|
|
40
|
+
xml_parts.append(arg_tag)
|
|
41
|
+
xml_parts.append(" </arguments>")
|
|
42
|
+
else:
|
|
43
|
+
xml_parts.append(" <!-- This tool takes no arguments -->")
|
|
44
|
+
|
|
45
|
+
xml_parts.append("</tool>")
|
|
46
|
+
return "\n".join(xml_parts)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/gemini_json_example_formatter.py
|
|
2
|
+
from typing import Dict, Any, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from autobyteus.tools.parameter_schema import ParameterType, ParameterDefinition
|
|
5
|
+
from .base_formatter import BaseExampleFormatter
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
9
|
+
|
|
10
|
+
class GeminiJsonExampleFormatter(BaseExampleFormatter):
|
|
11
|
+
"""Formats a tool usage example into the Google Gemini tool_calls format."""
|
|
12
|
+
|
|
13
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
14
|
+
tool_name = tool_definition.name
|
|
15
|
+
arg_schema = tool_definition.argument_schema
|
|
16
|
+
arguments = {}
|
|
17
|
+
|
|
18
|
+
if arg_schema and arg_schema.parameters:
|
|
19
|
+
for param_def in arg_schema.parameters:
|
|
20
|
+
if param_def.required or param_def.default_value is not None:
|
|
21
|
+
arguments[param_def.name] = self._generate_placeholder_value(param_def)
|
|
22
|
+
|
|
23
|
+
return {"name": tool_name, "args": arguments}
|
|
24
|
+
|
|
25
|
+
def _generate_placeholder_value(self, param_def: ParameterDefinition) -> Any:
|
|
26
|
+
if param_def.default_value is not None: return param_def.default_value
|
|
27
|
+
if param_def.param_type == ParameterType.STRING: return f"example_{param_def.name}"
|
|
28
|
+
if param_def.param_type == ParameterType.INTEGER: return 123
|
|
29
|
+
if param_def.param_type == ParameterType.FLOAT: return 123.45
|
|
30
|
+
if param_def.param_type == ParameterType.BOOLEAN: return True
|
|
31
|
+
if param_def.param_type == ParameterType.ENUM: return param_def.enum_values[0] if param_def.enum_values else "enum_val"
|
|
32
|
+
if param_def.param_type == ParameterType.OBJECT: return {"key": "value"}
|
|
33
|
+
if param_def.param_type == ParameterType.ARRAY: return ["item1", "item2"]
|
|
34
|
+
return "placeholder"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/gemini_json_schema_formatter.py
|
|
2
|
+
from typing import Dict, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from .base_formatter import BaseSchemaFormatter
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
8
|
+
|
|
9
|
+
class GeminiJsonSchemaFormatter(BaseSchemaFormatter):
|
|
10
|
+
"""Formats a tool's schema into a Google Gemini function declaration format."""
|
|
11
|
+
|
|
12
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
13
|
+
name = tool_definition.name
|
|
14
|
+
description = tool_definition.description
|
|
15
|
+
arg_schema = tool_definition.argument_schema
|
|
16
|
+
|
|
17
|
+
parameters = arg_schema.to_json_schema_dict() if arg_schema else {
|
|
18
|
+
"type": "object", "properties": {}, "required": []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"name": name,
|
|
23
|
+
"description": description,
|
|
24
|
+
"parameters": parameters,
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/google_json_example_formatter.py
|
|
2
|
+
from typing import Dict, Any, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from autobyteus.tools.parameter_schema import ParameterType, ParameterDefinition
|
|
5
|
+
from .base_formatter import BaseExampleFormatter
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
9
|
+
|
|
10
|
+
class GoogleJsonExampleFormatter(BaseExampleFormatter):
|
|
11
|
+
"""Formats a tool usage example into the Google JSON tool_calls format."""
|
|
12
|
+
|
|
13
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
14
|
+
tool_name = tool_definition.name
|
|
15
|
+
arg_schema = tool_definition.argument_schema
|
|
16
|
+
arguments = {}
|
|
17
|
+
|
|
18
|
+
if arg_schema and arg_schema.parameters:
|
|
19
|
+
for param_def in arg_schema.parameters:
|
|
20
|
+
if param_def.required or param_def.default_value is not None:
|
|
21
|
+
arguments[param_def.name] = self._generate_placeholder_value(param_def)
|
|
22
|
+
|
|
23
|
+
return {"name": tool_name, "args": arguments}
|
|
24
|
+
|
|
25
|
+
def _generate_placeholder_value(self, param_def: ParameterDefinition) -> Any:
|
|
26
|
+
if param_def.default_value is not None: return param_def.default_value
|
|
27
|
+
if param_def.param_type == ParameterType.STRING: return f"example_{param_def.name}"
|
|
28
|
+
if param_def.param_type == ParameterType.INTEGER: return 123
|
|
29
|
+
if param_def.param_type == ParameterType.FLOAT: return 123.45
|
|
30
|
+
if param_def.param_type == ParameterType.BOOLEAN: return True
|
|
31
|
+
if param_def.param_type == ParameterType.ENUM: return param_def.enum_values[0] if param_def.enum_values else "enum_val"
|
|
32
|
+
if param_def.param_type == ParameterType.OBJECT: return {"key": "value"}
|
|
33
|
+
if param_def.param_type == ParameterType.ARRAY: return ["item1", "item2"]
|
|
34
|
+
return "placeholder"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/google_json_schema_formatter.py
|
|
2
|
+
from typing import Dict, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from .base_formatter import BaseSchemaFormatter
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
8
|
+
|
|
9
|
+
class GoogleJsonSchemaFormatter(BaseSchemaFormatter):
|
|
10
|
+
"""Formats a tool's schema into a Google function declaration format."""
|
|
11
|
+
|
|
12
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
13
|
+
name = tool_definition.name
|
|
14
|
+
description = tool_definition.description
|
|
15
|
+
arg_schema = tool_definition.argument_schema
|
|
16
|
+
|
|
17
|
+
parameters = arg_schema.to_json_schema_dict() if arg_schema else {
|
|
18
|
+
"type": "object", "properties": {}, "required": []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"name": name,
|
|
23
|
+
"description": description,
|
|
24
|
+
"parameters": parameters,
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/openai_json_example_formatter.py
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Any, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from autobyteus.tools.parameter_schema import ParameterType, ParameterDefinition
|
|
6
|
+
from .base_formatter import BaseExampleFormatter
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
10
|
+
|
|
11
|
+
class OpenAiJsonExampleFormatter(BaseExampleFormatter):
|
|
12
|
+
"""
|
|
13
|
+
Formats a tool usage example into a format resembling an entry in the
|
|
14
|
+
OpenAI JSON 'tool_calls' array, intended for prompting a model.
|
|
15
|
+
The output is wrapped in a 'tool' key for consistency in prompts.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
19
|
+
tool_name = tool_definition.name
|
|
20
|
+
arg_schema = tool_definition.argument_schema
|
|
21
|
+
arguments = {}
|
|
22
|
+
|
|
23
|
+
if arg_schema and arg_schema.parameters:
|
|
24
|
+
for param_def in arg_schema.parameters:
|
|
25
|
+
if param_def.required or param_def.default_value is not None:
|
|
26
|
+
arguments[param_def.name] = self._generate_placeholder_value(param_def)
|
|
27
|
+
|
|
28
|
+
# This format contains the 'function' wrapper with a stringified 'arguments' field.
|
|
29
|
+
# This aligns with the structure often seen inside an OpenAI API tool_calls object.
|
|
30
|
+
function_call = {
|
|
31
|
+
"function": {
|
|
32
|
+
"name": tool_name,
|
|
33
|
+
"arguments": json.dumps(arguments),
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Wrap in a 'tool' key for consistency in prompt generation.
|
|
38
|
+
return {"tool": function_call}
|
|
39
|
+
|
|
40
|
+
def _generate_placeholder_value(self, param_def: ParameterDefinition) -> Any:
|
|
41
|
+
if param_def.default_value is not None: return param_def.default_value
|
|
42
|
+
if param_def.param_type == ParameterType.STRING: return f"example_{param_def.name}"
|
|
43
|
+
if param_def.param_type == ParameterType.INTEGER: return 123
|
|
44
|
+
if param_def.param_type == ParameterType.FLOAT: return 123.45
|
|
45
|
+
if param_def.param_type == ParameterType.BOOLEAN: return True
|
|
46
|
+
if param_def.param_type == ParameterType.ENUM: return param_def.enum_values[0] if param_def.enum_values else "enum_val"
|
|
47
|
+
if param_def.param_type == ParameterType.OBJECT: return {"key": "value"}
|
|
48
|
+
if param_def.param_type == ParameterType.ARRAY: return ["item1", "item2"]
|
|
49
|
+
return "placeholder"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/formatters/openai_json_schema_formatter.py
|
|
2
|
+
from typing import Dict, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from .base_formatter import BaseSchemaFormatter
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
8
|
+
|
|
9
|
+
class OpenAiJsonSchemaFormatter(BaseSchemaFormatter):
|
|
10
|
+
"""Formats a tool's schema into the OpenAI JSON function format."""
|
|
11
|
+
|
|
12
|
+
def provide(self, tool_definition: 'ToolDefinition') -> Dict:
|
|
13
|
+
name = tool_definition.name
|
|
14
|
+
description = tool_definition.description
|
|
15
|
+
arg_schema = tool_definition.argument_schema
|
|
16
|
+
|
|
17
|
+
parameters = arg_schema.to_json_schema_dict() if arg_schema else {
|
|
18
|
+
"type": "object", "properties": {}, "required": []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"type": "function",
|
|
23
|
+
"function": {
|
|
24
|
+
"name": name,
|
|
25
|
+
"description": description,
|
|
26
|
+
"parameters": parameters,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/parsers/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
This package contains concrete parser classes that translate an LLM's raw response
|
|
4
|
+
text into structured ToolInvocation objects.
|
|
5
|
+
"""
|
|
6
|
+
from .base_parser import BaseToolUsageParser
|
|
7
|
+
from .provider_aware_tool_usage_parser import ProviderAwareToolUsageParser
|
|
8
|
+
from .default_xml_tool_usage_parser import DefaultXmlToolUsageParser
|
|
9
|
+
from .anthropic_xml_tool_usage_parser import AnthropicXmlToolUsageParser
|
|
10
|
+
from .default_json_tool_usage_parser import DefaultJsonToolUsageParser
|
|
11
|
+
from .openai_json_tool_usage_parser import OpenAiJsonToolUsageParser
|
|
12
|
+
from .gemini_json_tool_usage_parser import GeminiJsonToolUsageParser
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BaseToolUsageParser",
|
|
16
|
+
"ProviderAwareToolUsageParser",
|
|
17
|
+
"DefaultXmlToolUsageParser",
|
|
18
|
+
"AnthropicXmlToolUsageParser",
|
|
19
|
+
"DefaultJsonToolUsageParser",
|
|
20
|
+
"OpenAiJsonToolUsageParser",
|
|
21
|
+
"GeminiJsonToolUsageParser",
|
|
22
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/parsers/anthropic_xml_tool_usage_parser.py
|
|
2
|
+
from .default_xml_tool_usage_parser import DefaultXmlToolUsageParser
|
|
3
|
+
|
|
4
|
+
class AnthropicXmlToolUsageParser(DefaultXmlToolUsageParser):
|
|
5
|
+
"""
|
|
6
|
+
Parser for Anthropic models. Anthropic uses XML for tool calls,
|
|
7
|
+
so this is an alias for the default XML parser.
|
|
8
|
+
"""
|
|
9
|
+
def get_name(self) -> str:
|
|
10
|
+
return "anthropic_xml_tool_usage_parser"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/parsers/base_parser.py
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
8
|
+
from autobyteus.llm.utils.response_types import CompleteResponse
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class BaseToolUsageParser(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for parsing tool usage from an LLM's response text.
|
|
15
|
+
These parsers are responsible for extracting structured tool call information.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def get_name(self) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Returns the unique name for this parser.
|
|
21
|
+
Defaults to the class name.
|
|
22
|
+
"""
|
|
23
|
+
return self.__class__.__name__
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def parse(self, response: 'CompleteResponse') -> List['ToolInvocation']:
|
|
27
|
+
"""
|
|
28
|
+
Parses the LLM's response. If actionable tool calls are found,
|
|
29
|
+
this method should return a list of ToolInvocation objects.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
response: The CompleteResponse object from the LLM.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A list of ToolInvocation objects. Returns an empty list if no
|
|
36
|
+
valid tool calls are found.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError("Subclasses must implement the 'parse' method.")
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return f"<{self.__class__.__name__}>"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING, List
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
9
|
+
from .base_parser import BaseToolUsageParser
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from autobyteus.llm.utils.response_types import CompleteResponse
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class DefaultJsonToolUsageParser(BaseToolUsageParser):
|
|
17
|
+
"""
|
|
18
|
+
A default parser for tool usage commands formatted as custom JSON.
|
|
19
|
+
It expects a 'tool' object with 'function' and 'parameters' keys.
|
|
20
|
+
"""
|
|
21
|
+
def get_name(self) -> str:
|
|
22
|
+
return "default_json_tool_usage_parser"
|
|
23
|
+
|
|
24
|
+
def parse(self, response: 'CompleteResponse') -> List[ToolInvocation]:
|
|
25
|
+
response_text = self._extract_json_from_response(response.content)
|
|
26
|
+
if not response_text:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(response_text)
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
logger.debug(f"Could not parse extracted text as JSON. Text: {response_text[:200]}")
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
tool_calls_data = []
|
|
36
|
+
if isinstance(data, list):
|
|
37
|
+
tool_calls_data = data
|
|
38
|
+
elif isinstance(data, dict):
|
|
39
|
+
if "tools" in data and isinstance(data.get("tools"), list):
|
|
40
|
+
tool_calls_data = data["tools"]
|
|
41
|
+
else:
|
|
42
|
+
tool_calls_data = [data]
|
|
43
|
+
else:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
invocations: List[ToolInvocation] = []
|
|
47
|
+
for call_data in tool_calls_data:
|
|
48
|
+
if not isinstance(call_data, dict):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
tool_block = call_data.get("tool")
|
|
52
|
+
if not isinstance(tool_block, dict):
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
tool_name = tool_block.get("function")
|
|
56
|
+
arguments = tool_block.get("parameters")
|
|
57
|
+
|
|
58
|
+
if not tool_name or not isinstance(tool_name, str):
|
|
59
|
+
logger.debug(f"Skipping malformed tool block (missing or invalid 'function'): {tool_block}")
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if arguments is None:
|
|
63
|
+
arguments = {}
|
|
64
|
+
|
|
65
|
+
if not isinstance(arguments, dict):
|
|
66
|
+
logger.debug(f"Skipping tool block with invalid 'parameters' type ({type(arguments)}): {tool_block}")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# The custom format does not have a tool ID, so we generate one.
|
|
70
|
+
tool_id = str(uuid.uuid4())
|
|
71
|
+
try:
|
|
72
|
+
tool_invocation = ToolInvocation(name=tool_name, arguments=arguments, id=tool_id)
|
|
73
|
+
invocations.append(tool_invocation)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Unexpected error creating ToolInvocation for tool '{tool_name}': {e}", exc_info=True)
|
|
76
|
+
|
|
77
|
+
return invocations
|
|
78
|
+
|
|
79
|
+
def _extract_json_from_response(self, text: str) -> Optional[str]:
|
|
80
|
+
match = re.search(r"```(?:json)?\s*([\s\S]+?)\s*```", text)
|
|
81
|
+
if match:
|
|
82
|
+
return match.group(1).strip()
|
|
83
|
+
|
|
84
|
+
# Try to find a JSON object or array in the text
|
|
85
|
+
first_bracket = text.find('[')
|
|
86
|
+
first_brace = text.find('{')
|
|
87
|
+
|
|
88
|
+
if first_brace == -1 and first_bracket == -1:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
start_index = -1
|
|
92
|
+
if first_bracket != -1 and first_brace != -1:
|
|
93
|
+
start_index = min(first_bracket, first_brace)
|
|
94
|
+
elif first_bracket != -1:
|
|
95
|
+
start_index = first_bracket
|
|
96
|
+
else: # first_brace != -1
|
|
97
|
+
start_index = first_brace
|
|
98
|
+
|
|
99
|
+
json_substring = text[start_index:]
|
|
100
|
+
try:
|
|
101
|
+
# Check if the substring is valid JSON
|
|
102
|
+
json.loads(json_substring)
|
|
103
|
+
return json_substring
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
logger.debug(f"Found potential start of JSON, but substring was not valid: {json_substring[:100]}")
|
|
106
|
+
return None
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
import re
|
|
4
|
+
import uuid
|
|
5
|
+
from xml.sax.saxutils import escape, unescape
|
|
6
|
+
import xml.parsers.expat
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING, Dict, Any, List
|
|
9
|
+
|
|
10
|
+
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
11
|
+
from .base_parser import BaseToolUsageParser
|
|
12
|
+
from .exceptions import ToolUsageParseException
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from autobyteus.llm.utils.response_types import CompleteResponse
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class DefaultXmlToolUsageParser(BaseToolUsageParser):
|
|
20
|
+
"""
|
|
21
|
+
Parses LLM responses for tool usage commands formatted as XML.
|
|
22
|
+
It looks for either a <tools> block (for multiple calls) or a
|
|
23
|
+
single <tool> block.
|
|
24
|
+
"""
|
|
25
|
+
def get_name(self) -> str:
|
|
26
|
+
return "default_xml_tool_usage_parser"
|
|
27
|
+
|
|
28
|
+
def parse(self, response: 'CompleteResponse') -> List[ToolInvocation]:
|
|
29
|
+
response_text = response.content
|
|
30
|
+
logger.debug(f"{self.get_name()} attempting to parse response (first 500 chars): {response_text[:500]}...")
|
|
31
|
+
|
|
32
|
+
invocations: List[ToolInvocation] = []
|
|
33
|
+
match = re.search(r"<tools\b[^>]*>.*?</tools\s*>|<tool\b[^>]*>.*?</tool\s*>", response_text, re.DOTALL | re.IGNORECASE)
|
|
34
|
+
if not match:
|
|
35
|
+
logger.debug(f"No <tools> or <tool> block found by {self.get_name()}.")
|
|
36
|
+
return invocations
|
|
37
|
+
|
|
38
|
+
xml_content = match.group(0)
|
|
39
|
+
processed_xml = self._preprocess_xml_for_parsing(xml_content)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
root = ET.fromstring(processed_xml)
|
|
43
|
+
tool_elements = []
|
|
44
|
+
|
|
45
|
+
if root.tag.lower() == "tools":
|
|
46
|
+
tool_elements = root.findall('tool')
|
|
47
|
+
if not tool_elements:
|
|
48
|
+
logger.debug("Found <tools> but no <tool> children.")
|
|
49
|
+
return invocations
|
|
50
|
+
elif root.tag.lower() == "tool":
|
|
51
|
+
tool_elements = [root]
|
|
52
|
+
else:
|
|
53
|
+
logger.warning(f"Root XML tag is '{root.tag}', not 'tools' or 'tool'. Skipping parsing.")
|
|
54
|
+
return invocations
|
|
55
|
+
|
|
56
|
+
for tool_elem in tool_elements:
|
|
57
|
+
tool_name = tool_elem.attrib.get("name")
|
|
58
|
+
tool_id = tool_elem.attrib.get("id") or str(uuid.uuid4())
|
|
59
|
+
arguments = self._parse_arguments_from_xml(tool_elem)
|
|
60
|
+
|
|
61
|
+
if tool_name:
|
|
62
|
+
tool_invocation = ToolInvocation(name=tool_name, arguments=arguments, id=tool_id)
|
|
63
|
+
invocations.append(tool_invocation)
|
|
64
|
+
else:
|
|
65
|
+
logger.warning(f"Parsed a <tool> element but its 'name' attribute is missing or empty.")
|
|
66
|
+
|
|
67
|
+
except (ET.ParseError, xml.parsers.expat.ExpatError) as e:
|
|
68
|
+
error_msg = f"XML parsing error in '{self.get_name()}': {e}. Content: '{processed_xml[:200]}'"
|
|
69
|
+
logger.debug(error_msg)
|
|
70
|
+
# Raise a specific exception to be caught upstream.
|
|
71
|
+
raise ToolUsageParseException(error_msg, original_exception=e)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Unexpected error in {self.get_name()} processing XML: {e}. XML Content: {xml_content[:200]}", exc_info=True)
|
|
75
|
+
# Also wrap unexpected errors for consistent handling.
|
|
76
|
+
raise ToolUsageParseException(f"Unexpected error during XML parsing: {e}", original_exception=e)
|
|
77
|
+
|
|
78
|
+
return invocations
|
|
79
|
+
|
|
80
|
+
def _preprocess_xml_for_parsing(self, xml_content: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Preprocesses raw XML string from an LLM to fix common errors before parsing.
|
|
83
|
+
"""
|
|
84
|
+
processed_content = re.sub(
|
|
85
|
+
r'(<arg\s+name\s*=\s*")([^"]+?)>',
|
|
86
|
+
r'\1\2">',
|
|
87
|
+
xml_content,
|
|
88
|
+
flags=re.IGNORECASE
|
|
89
|
+
)
|
|
90
|
+
if processed_content != xml_content:
|
|
91
|
+
logger.debug("Preprocessor fixed a missing quote in an <arg> tag.")
|
|
92
|
+
|
|
93
|
+
cdata_sections: Dict[str, str] = {}
|
|
94
|
+
def cdata_replacer(match_obj: re.Match) -> str:
|
|
95
|
+
placeholder = f"__CDATA_PLACEHOLDER_{len(cdata_sections)}__"
|
|
96
|
+
cdata_sections[placeholder] = match_obj.group(0)
|
|
97
|
+
return placeholder
|
|
98
|
+
|
|
99
|
+
xml_no_cdata = re.sub(r'<!\[CDATA\[.*?\]\]>', cdata_replacer, processed_content, flags=re.DOTALL)
|
|
100
|
+
|
|
101
|
+
def escape_arg_value(match_obj: re.Match) -> str:
|
|
102
|
+
open_tag = match_obj.group(1)
|
|
103
|
+
content = match_obj.group(2)
|
|
104
|
+
close_tag = match_obj.group(3)
|
|
105
|
+
if re.search(r'<\s*/?[a-zA-Z]', content.strip()):
|
|
106
|
+
return f"{open_tag}{content}{close_tag}"
|
|
107
|
+
escaped_content = escape(content) if not content.startswith("__CDATA_PLACEHOLDER_") else content
|
|
108
|
+
return f"{open_tag}{escaped_content}{close_tag}"
|
|
109
|
+
|
|
110
|
+
processed_content = re.sub(
|
|
111
|
+
r'(<arg\s+name\s*=\s*"[^"]*"\s*>\s*)(.*?)(\s*</arg\s*>)',
|
|
112
|
+
escape_arg_value,
|
|
113
|
+
xml_no_cdata,
|
|
114
|
+
flags=re.DOTALL | re.IGNORECASE
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for placeholder, original_cdata_tag in cdata_sections.items():
|
|
118
|
+
processed_content = processed_content.replace(placeholder, original_cdata_tag)
|
|
119
|
+
|
|
120
|
+
return processed_content
|
|
121
|
+
|
|
122
|
+
def _parse_arguments_from_xml(self, command_element: ET.Element) -> Dict[str, Any]:
|
|
123
|
+
arguments: Dict[str, Any] = {}
|
|
124
|
+
arguments_container = command_element.find('arguments')
|
|
125
|
+
if arguments_container is None:
|
|
126
|
+
logger.debug(f"No <arguments> tag found in <tool name='{command_element.attrib.get('name')}'>. No arguments will be parsed.")
|
|
127
|
+
return arguments
|
|
128
|
+
|
|
129
|
+
for arg_element in arguments_container.findall('arg'):
|
|
130
|
+
arg_name = arg_element.attrib.get('name')
|
|
131
|
+
if arg_name:
|
|
132
|
+
raw_text = "".join(arg_element.itertext())
|
|
133
|
+
unescaped_value = unescape(raw_text)
|
|
134
|
+
arguments[arg_name] = unescaped_value
|
|
135
|
+
return arguments
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/usage/parsers/exceptions.py
|
|
2
|
+
"""
|
|
3
|
+
Custom exceptions for the tool usage parsing module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
class ToolUsageParseException(Exception):
|
|
7
|
+
"""
|
|
8
|
+
Raised when a tool usage parser fails to parse an LLM response due to
|
|
9
|
+
malformed content or other parsing errors.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, message: str, original_exception: Exception = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.original_exception = original_exception
|