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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from autobyteus.tools.base_tool import BaseTool
|
|
6
|
+
from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
7
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
8
|
+
from autobyteus.multimedia.image import image_client_factory, ImageModel, ImageClientFactory
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_configured_model_identifier(env_var: str, default_model: Optional[str] = None) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Retrieves a model identifier from an environment variable, with a fallback to a default.
|
|
16
|
+
"""
|
|
17
|
+
model_identifier = os.getenv(env_var)
|
|
18
|
+
if not model_identifier:
|
|
19
|
+
if default_model:
|
|
20
|
+
return default_model
|
|
21
|
+
raise ValueError(f"The '{env_var}' environment variable is not set. Please configure it.")
|
|
22
|
+
return model_identifier
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_dynamic_image_schema(base_params: List[ParameterDefinition], model_env_var: str, default_model: str) -> ParameterSchema:
|
|
26
|
+
"""
|
|
27
|
+
Builds the tool schema dynamically based on the configured image model.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
model_identifier = _get_configured_model_identifier(model_env_var, default_model)
|
|
31
|
+
ImageClientFactory.ensure_initialized()
|
|
32
|
+
model = ImageModel[model_identifier]
|
|
33
|
+
except (ValueError, KeyError) as e:
|
|
34
|
+
logger.error(f"Cannot generate image tool schema. Check environment and model registry. Error: {e}")
|
|
35
|
+
raise RuntimeError(f"Failed to configure image tool. Error: {e}")
|
|
36
|
+
|
|
37
|
+
config_schema = ParameterSchema()
|
|
38
|
+
if model.parameter_schema:
|
|
39
|
+
for name, meta in model.parameter_schema.items():
|
|
40
|
+
param_type_str = meta.get("type", "string").upper()
|
|
41
|
+
param_type = getattr(ParameterType, param_type_str, ParameterType.STRING)
|
|
42
|
+
|
|
43
|
+
allowed_values = meta.get("allowed_values")
|
|
44
|
+
if param_type == ParameterType.STRING and allowed_values:
|
|
45
|
+
param_type = ParameterType.ENUM
|
|
46
|
+
|
|
47
|
+
config_schema.add_parameter(ParameterDefinition(
|
|
48
|
+
name=name,
|
|
49
|
+
param_type=param_type,
|
|
50
|
+
description=meta.get("description", ""),
|
|
51
|
+
required=False,
|
|
52
|
+
default_value=meta.get("default"),
|
|
53
|
+
enum_values=allowed_values
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
schema = ParameterSchema()
|
|
57
|
+
for param in base_params:
|
|
58
|
+
schema.add_parameter(param)
|
|
59
|
+
|
|
60
|
+
if config_schema.parameters:
|
|
61
|
+
schema.add_parameter(ParameterDefinition(
|
|
62
|
+
name="generation_config",
|
|
63
|
+
param_type=ParameterType.OBJECT,
|
|
64
|
+
description=f"Model-specific generation parameters for the configured '{model_identifier}' model.",
|
|
65
|
+
required=False,
|
|
66
|
+
object_schema=config_schema
|
|
67
|
+
))
|
|
68
|
+
return schema
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GenerateImageTool(BaseTool):
|
|
72
|
+
"""
|
|
73
|
+
An agent tool for generating images from a text prompt using a pre-configured model.
|
|
74
|
+
"""
|
|
75
|
+
CATEGORY = ToolCategory.MULTIMEDIA
|
|
76
|
+
MODEL_ENV_VAR = "DEFAULT_IMAGE_GENERATION_MODEL"
|
|
77
|
+
DEFAULT_MODEL = "gpt-image-1"
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def get_name(cls) -> str:
|
|
81
|
+
return "GenerateImage"
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def get_description(cls) -> str:
|
|
85
|
+
return (
|
|
86
|
+
"Generates one or more images based on a textual description (prompt) using the system's default image model. "
|
|
87
|
+
"Returns a list of URLs to the generated images upon success."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_argument_schema(cls) -> Optional[ParameterSchema]:
|
|
92
|
+
base_params = [
|
|
93
|
+
ParameterDefinition(
|
|
94
|
+
name="prompt",
|
|
95
|
+
param_type=ParameterType.STRING,
|
|
96
|
+
description="A detailed textual description of the image to generate.",
|
|
97
|
+
required=True
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
return _build_dynamic_image_schema(base_params, cls.MODEL_ENV_VAR, cls.DEFAULT_MODEL)
|
|
101
|
+
|
|
102
|
+
async def _execute(self, context, prompt: str, generation_config: Optional[dict] = None) -> List[str]:
|
|
103
|
+
model_identifier = _get_configured_model_identifier(self.MODEL_ENV_VAR, self.DEFAULT_MODEL)
|
|
104
|
+
logger.info(f"GenerateImageTool executing with configured model '{model_identifier}'.")
|
|
105
|
+
client = None
|
|
106
|
+
try:
|
|
107
|
+
client = image_client_factory.create_image_client(model_identifier=model_identifier)
|
|
108
|
+
response = await client.generate_image(prompt=prompt, generation_config=generation_config)
|
|
109
|
+
|
|
110
|
+
if not response.image_urls:
|
|
111
|
+
raise ValueError("Image generation failed to return any image URLs.")
|
|
112
|
+
|
|
113
|
+
return response.image_urls
|
|
114
|
+
finally:
|
|
115
|
+
if client:
|
|
116
|
+
await client.cleanup()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class EditImageTool(BaseTool):
|
|
120
|
+
"""
|
|
121
|
+
An agent tool for editing an existing image using a text prompt and a pre-configured model.
|
|
122
|
+
"""
|
|
123
|
+
CATEGORY = ToolCategory.MULTIMEDIA
|
|
124
|
+
MODEL_ENV_VAR = "DEFAULT_IMAGE_GENERATION_MODEL"
|
|
125
|
+
DEFAULT_MODEL = "gpt-image-1"
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_name(cls) -> str:
|
|
129
|
+
return "EditImage"
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def get_description(cls) -> str:
|
|
133
|
+
return (
|
|
134
|
+
"Edits an existing image based on a textual description (prompt) using the system's default image model. "
|
|
135
|
+
"A mask can be provided to specify the exact area to edit (inpainting). "
|
|
136
|
+
"Returns a list of URLs to the edited images."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def get_argument_schema(cls) -> Optional[ParameterSchema]:
|
|
141
|
+
base_params = [
|
|
142
|
+
ParameterDefinition(
|
|
143
|
+
name="prompt",
|
|
144
|
+
param_type=ParameterType.STRING,
|
|
145
|
+
description="A detailed textual description of the edits to apply to the image.",
|
|
146
|
+
required=True
|
|
147
|
+
),
|
|
148
|
+
ParameterDefinition(
|
|
149
|
+
name="input_image_urls",
|
|
150
|
+
param_type=ParameterType.STRING,
|
|
151
|
+
description="A comma-separated string of URLs to the source images that need to be edited. Some models may only use the first URL.",
|
|
152
|
+
required=True
|
|
153
|
+
),
|
|
154
|
+
ParameterDefinition(
|
|
155
|
+
name="mask_image_url",
|
|
156
|
+
param_type=ParameterType.STRING,
|
|
157
|
+
description="Optional. A URL to a mask image (PNG). The transparent areas of this mask define where the input image should be edited.",
|
|
158
|
+
required=False
|
|
159
|
+
)
|
|
160
|
+
]
|
|
161
|
+
return _build_dynamic_image_schema(base_params, cls.MODEL_ENV_VAR, cls.DEFAULT_MODEL)
|
|
162
|
+
|
|
163
|
+
async def _execute(self, context, prompt: str, input_image_urls: str, generation_config: Optional[dict] = None, mask_image_url: Optional[str] = None) -> List[str]:
|
|
164
|
+
model_identifier = _get_configured_model_identifier(self.MODEL_ENV_VAR, self.DEFAULT_MODEL)
|
|
165
|
+
logger.info(f"EditImageTool executing with configured model '{model_identifier}'.")
|
|
166
|
+
client = None
|
|
167
|
+
try:
|
|
168
|
+
urls_list = [url.strip() for url in input_image_urls.split(',') if url.strip()]
|
|
169
|
+
if not urls_list:
|
|
170
|
+
raise ValueError("The 'input_image_urls' parameter cannot be empty.")
|
|
171
|
+
|
|
172
|
+
client = image_client_factory.create_image_client(model_identifier=model_identifier)
|
|
173
|
+
response = await client.edit_image(
|
|
174
|
+
prompt=prompt,
|
|
175
|
+
input_image_urls=urls_list,
|
|
176
|
+
mask_url=mask_image_url,
|
|
177
|
+
generation_config=generation_config
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if not response.image_urls:
|
|
181
|
+
raise ValueError("Image editing failed to return any image URLs.")
|
|
182
|
+
|
|
183
|
+
return response.image_urls
|
|
184
|
+
finally:
|
|
185
|
+
if client:
|
|
186
|
+
await client.cleanup()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/tools/parameter_schema.py
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import logging
|
|
3
4
|
from typing import Dict, Any, List, Optional, Union, Type
|
|
4
5
|
from dataclasses import dataclass, field
|
|
@@ -41,8 +42,9 @@ class ParameterDefinition:
|
|
|
41
42
|
min_value: Optional[Union[int, float]] = None
|
|
42
43
|
max_value: Optional[Union[int, float]] = None
|
|
43
44
|
pattern: Optional[str] = None
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
# FIX: Allow dict for raw JSON schemas, in addition to ParameterType and ParameterSchema.
|
|
46
|
+
array_item_schema: Optional[Union[ParameterType, ParameterSchema, dict]] = None
|
|
47
|
+
object_schema: Optional[ParameterSchema] = None
|
|
46
48
|
|
|
47
49
|
def __post_init__(self):
|
|
48
50
|
if not self.name or not isinstance(self.name, str):
|
|
@@ -54,8 +56,15 @@ class ParameterDefinition:
|
|
|
54
56
|
if self.param_type == ParameterType.ENUM and not self.enum_values:
|
|
55
57
|
raise ValueError(f"ParameterDefinition '{self.name}' of type ENUM must specify enum_values")
|
|
56
58
|
|
|
59
|
+
# FIX: Update validation to allow dict for array_item_schema.
|
|
60
|
+
if self.array_item_schema is not None and not isinstance(self.array_item_schema, (ParameterType, ParameterSchema, dict)):
|
|
61
|
+
raise ValueError(f"ParameterDefinition '{self.name}': array_item_schema must be a ParameterType, ParameterSchema, or dict instance.")
|
|
62
|
+
|
|
63
|
+
if self.object_schema is not None and not isinstance(self.object_schema, ParameterSchema):
|
|
64
|
+
raise ValueError(f"ParameterDefinition '{self.name}': object_schema must be a ParameterSchema instance.")
|
|
65
|
+
|
|
57
66
|
if self.param_type == ParameterType.ARRAY and self.array_item_schema is None:
|
|
58
|
-
logger.debug(f"ParameterDefinition '{self.name}' of type ARRAY has no
|
|
67
|
+
logger.debug(f"ParameterDefinition '{self.name}' of type ARRAY has no item schema. Will be a generic array of any type.")
|
|
59
68
|
|
|
60
69
|
if self.param_type != ParameterType.ARRAY and self.array_item_schema is not None:
|
|
61
70
|
raise ValueError(f"ParameterDefinition '{self.name}': array_item_schema should only be provided if param_type is ARRAY.")
|
|
@@ -71,44 +80,30 @@ class ParameterDefinition:
|
|
|
71
80
|
return not self.required
|
|
72
81
|
|
|
73
82
|
if self.param_type == ParameterType.STRING:
|
|
74
|
-
if not isinstance(value, str):
|
|
75
|
-
|
|
76
|
-
if self.pattern:
|
|
77
|
-
if not re.match(self.pattern, value):
|
|
78
|
-
return False
|
|
83
|
+
if not isinstance(value, str): return False
|
|
84
|
+
if self.pattern and not re.match(self.pattern, value): return False
|
|
79
85
|
|
|
80
86
|
elif self.param_type == ParameterType.INTEGER:
|
|
81
|
-
if not isinstance(value, int):
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if self.min_value is not None and value < self.min_value:
|
|
85
|
-
return False
|
|
86
|
-
if self.max_value is not None and value > self.max_value:
|
|
87
|
-
return False
|
|
87
|
+
if not isinstance(value, int) or isinstance(value, bool): return False
|
|
88
|
+
if self.min_value is not None and value < self.min_value: return False
|
|
89
|
+
if self.max_value is not None and value > self.max_value: return False
|
|
88
90
|
|
|
89
91
|
elif self.param_type == ParameterType.FLOAT:
|
|
90
|
-
if not isinstance(value, (float, int)):
|
|
91
|
-
|
|
92
|
-
if self.
|
|
93
|
-
return False
|
|
94
|
-
if self.max_value is not None and float(value) > self.max_value:
|
|
95
|
-
return False
|
|
92
|
+
if not isinstance(value, (float, int)): return False
|
|
93
|
+
if self.min_value is not None and float(value) < self.min_value: return False
|
|
94
|
+
if self.max_value is not None and float(value) > self.max_value: return False
|
|
96
95
|
|
|
97
96
|
elif self.param_type == ParameterType.BOOLEAN:
|
|
98
|
-
if not isinstance(value, bool):
|
|
99
|
-
return False
|
|
97
|
+
if not isinstance(value, bool): return False
|
|
100
98
|
|
|
101
99
|
elif self.param_type == ParameterType.ENUM:
|
|
102
|
-
if not isinstance(value, str) or value not in (self.enum_values or []):
|
|
103
|
-
return False
|
|
100
|
+
if not isinstance(value, str) or value not in (self.enum_values or []): return False
|
|
104
101
|
|
|
105
102
|
elif self.param_type == ParameterType.OBJECT:
|
|
106
|
-
if not isinstance(value, dict):
|
|
107
|
-
return False
|
|
103
|
+
if not isinstance(value, dict): return False
|
|
108
104
|
|
|
109
105
|
elif self.param_type == ParameterType.ARRAY:
|
|
110
|
-
if not isinstance(value, list):
|
|
111
|
-
return False
|
|
106
|
+
if not isinstance(value, list): return False
|
|
112
107
|
|
|
113
108
|
return True
|
|
114
109
|
|
|
@@ -124,17 +119,22 @@ class ParameterDefinition:
|
|
|
124
119
|
"max_value": self.max_value,
|
|
125
120
|
"pattern": self.pattern,
|
|
126
121
|
}
|
|
122
|
+
# FIX: Correctly serialize dicts for array_item_schema.
|
|
127
123
|
if self.param_type == ParameterType.ARRAY and self.array_item_schema is not None:
|
|
128
|
-
|
|
124
|
+
if isinstance(self.array_item_schema, ParameterSchema):
|
|
125
|
+
data["array_item_schema"] = self.array_item_schema.to_dict()
|
|
126
|
+
elif isinstance(self.array_item_schema, dict):
|
|
127
|
+
data["array_item_schema"] = self.array_item_schema
|
|
128
|
+
elif isinstance(self.array_item_schema, ParameterType):
|
|
129
|
+
data["array_item_schema"] = {"type": self.array_item_schema.value}
|
|
130
|
+
|
|
129
131
|
if self.param_type == ParameterType.OBJECT and self.object_schema is not None:
|
|
130
|
-
data["object_schema"] = self.object_schema
|
|
132
|
+
data["object_schema"] = self.object_schema.to_dict()
|
|
131
133
|
return data
|
|
132
134
|
|
|
133
135
|
def to_json_schema_property_dict(self) -> Dict[str, Any]:
|
|
134
136
|
if self.param_type == ParameterType.OBJECT and self.object_schema:
|
|
135
|
-
|
|
136
|
-
# We add the description at the top level for clarity.
|
|
137
|
-
schema = self.object_schema.copy()
|
|
137
|
+
schema = self.object_schema.to_json_schema_dict()
|
|
138
138
|
schema["description"] = self.description
|
|
139
139
|
return schema
|
|
140
140
|
|
|
@@ -148,40 +148,37 @@ class ParameterDefinition:
|
|
|
148
148
|
if self.param_type == ParameterType.ENUM and self.enum_values:
|
|
149
149
|
prop_dict["enum"] = self.enum_values
|
|
150
150
|
|
|
151
|
-
if self.min_value is not None:
|
|
152
|
-
|
|
153
|
-
prop_dict["minimum"] = self.min_value
|
|
151
|
+
if self.min_value is not None and self.param_type in [ParameterType.INTEGER, ParameterType.FLOAT]:
|
|
152
|
+
prop_dict["minimum"] = self.min_value
|
|
154
153
|
|
|
155
|
-
if self.max_value is not None:
|
|
156
|
-
|
|
157
|
-
prop_dict["maximum"] = self.max_value
|
|
154
|
+
if self.max_value is not None and self.param_type in [ParameterType.INTEGER, ParameterType.FLOAT]:
|
|
155
|
+
prop_dict["maximum"] = self.max_value
|
|
158
156
|
|
|
159
157
|
if self.pattern and self.param_type == ParameterType.STRING:
|
|
160
158
|
prop_dict["pattern"] = self.pattern
|
|
161
159
|
|
|
160
|
+
# FIX: Correctly handle dicts when generating the 'items' part of an array schema.
|
|
162
161
|
if self.param_type == ParameterType.ARRAY:
|
|
163
|
-
if self.array_item_schema
|
|
162
|
+
if isinstance(self.array_item_schema, ParameterSchema):
|
|
163
|
+
prop_dict["items"] = self.array_item_schema.to_json_schema_dict()
|
|
164
|
+
elif isinstance(self.array_item_schema, dict):
|
|
164
165
|
prop_dict["items"] = self.array_item_schema
|
|
166
|
+
elif isinstance(self.array_item_schema, ParameterType):
|
|
167
|
+
prop_dict["items"] = {"type": self.array_item_schema.to_json_schema_type()}
|
|
165
168
|
else:
|
|
166
169
|
prop_dict["items"] = True
|
|
167
|
-
logger.debug(f"Parameter '{self.name}' is ARRAY type with no item schema; JSON schema 'items' will be generic.")
|
|
168
170
|
|
|
169
171
|
return prop_dict
|
|
170
172
|
|
|
171
173
|
@dataclass
|
|
172
174
|
class ParameterSchema:
|
|
173
|
-
"""
|
|
174
|
-
Describes a schema for a set of parameters, either for tool arguments or instantiation configuration.
|
|
175
|
-
"""
|
|
176
175
|
parameters: List[ParameterDefinition] = field(default_factory=list)
|
|
177
176
|
|
|
178
177
|
def add_parameter(self, parameter: ParameterDefinition) -> None:
|
|
179
178
|
if not isinstance(parameter, ParameterDefinition):
|
|
180
179
|
raise TypeError("parameter must be a ParameterDefinition instance")
|
|
181
|
-
|
|
182
180
|
if any(p.name == parameter.name for p in self.parameters):
|
|
183
181
|
raise ValueError(f"Parameter '{parameter.name}' already exists in schema")
|
|
184
|
-
|
|
185
182
|
self.parameters.append(parameter)
|
|
186
183
|
|
|
187
184
|
def get_parameter(self, name: str) -> Optional[ParameterDefinition]:
|
|
@@ -189,67 +186,64 @@ class ParameterSchema:
|
|
|
189
186
|
|
|
190
187
|
def validate_config(self, config_data: Dict[str, Any]) -> tuple[bool, List[str]]:
|
|
191
188
|
errors = []
|
|
192
|
-
|
|
193
189
|
for param_def in self.parameters:
|
|
194
190
|
if param_def.required and param_def.name not in config_data:
|
|
195
191
|
errors.append(f"Required parameter '{param_def.name}' is missing.")
|
|
196
|
-
|
|
197
192
|
for key, value in config_data.items():
|
|
198
193
|
param_def = self.get_parameter(key)
|
|
199
194
|
if not param_def:
|
|
200
|
-
logger.debug(f"Unknown parameter '{key}' provided. It will be ignored
|
|
195
|
+
logger.debug(f"Unknown parameter '{key}' provided. It will be ignored.")
|
|
201
196
|
continue
|
|
202
|
-
|
|
203
197
|
if not param_def.validate_value(value):
|
|
204
198
|
errors.append(f"Invalid value for parameter '{param_def.name}': '{str(value)[:50]}...'. Expected type compatible with {param_def.param_type.value}.")
|
|
205
|
-
|
|
206
199
|
return len(errors) == 0, errors
|
|
207
200
|
|
|
208
|
-
def get_defaults(self) -> Dict[str, Any]:
|
|
209
|
-
return {
|
|
210
|
-
param.name: param.default_value
|
|
211
|
-
for param in self.parameters
|
|
212
|
-
if param.default_value is not None
|
|
213
|
-
}
|
|
214
|
-
|
|
215
201
|
def to_dict(self) -> Dict[str, Any]:
|
|
216
|
-
return {
|
|
217
|
-
"parameters": [param.to_dict() for param in self.parameters]
|
|
218
|
-
}
|
|
202
|
+
return {"parameters": [param.to_dict() for param in self.parameters]}
|
|
219
203
|
|
|
220
204
|
def to_json_schema_dict(self) -> Dict[str, Any]:
|
|
221
205
|
if not self.parameters:
|
|
222
|
-
return {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
properties = {
|
|
229
|
-
param.name: param.to_json_schema_property_dict()
|
|
230
|
-
for param in self.parameters
|
|
231
|
-
}
|
|
232
|
-
required = [
|
|
233
|
-
param.name for param in self.parameters if param.required
|
|
234
|
-
]
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
"type": "object",
|
|
238
|
-
"properties": properties,
|
|
239
|
-
"required": required,
|
|
240
|
-
}
|
|
206
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
207
|
+
properties = {p.name: p.to_json_schema_property_dict() for p in self.parameters}
|
|
208
|
+
required = [p.name for p in self.parameters if p.required]
|
|
209
|
+
return {"type": "object", "properties": properties, "required": required}
|
|
241
210
|
|
|
242
211
|
@classmethod
|
|
243
212
|
def from_dict(cls, schema_data: Dict[str, Any]) -> 'ParameterSchema':
|
|
244
213
|
schema = cls()
|
|
245
|
-
|
|
246
214
|
for param_data in schema_data.get("parameters", []):
|
|
247
|
-
param_type_value = param_data["type"]
|
|
248
215
|
try:
|
|
249
|
-
param_type_enum = ParameterType(
|
|
216
|
+
param_type_enum = ParameterType(param_data["type"])
|
|
250
217
|
except ValueError:
|
|
251
|
-
raise ValueError(f"Invalid parameter type string '{
|
|
218
|
+
raise ValueError(f"Invalid parameter type string '{param_data['type']}' for param '{param_data.get('name')}'.")
|
|
219
|
+
|
|
220
|
+
array_schema_obj = None
|
|
221
|
+
if "array_item_schema" in param_data and param_data["array_item_schema"] is not None:
|
|
222
|
+
item_schema_data = param_data["array_item_schema"]
|
|
223
|
+
|
|
224
|
+
# FIX: Add robust logic to deserialize array_item_schema correctly.
|
|
225
|
+
if isinstance(item_schema_data, dict):
|
|
226
|
+
if "parameters" in item_schema_data:
|
|
227
|
+
# It's our internal ParameterSchema format.
|
|
228
|
+
array_schema_obj = ParameterSchema.from_dict(item_schema_data)
|
|
229
|
+
elif "type" in item_schema_data and len(item_schema_data) == 1:
|
|
230
|
+
# Heuristic: it's a simple primitive type like {'type': 'string'}.
|
|
231
|
+
try:
|
|
232
|
+
array_schema_obj = ParameterType(item_schema_data["type"])
|
|
233
|
+
except ValueError:
|
|
234
|
+
# Not a valid ParameterType, so treat it as a raw schema dict.
|
|
235
|
+
array_schema_obj = item_schema_data
|
|
236
|
+
else:
|
|
237
|
+
# It's a complex JSON schema dict, store it as is.
|
|
238
|
+
array_schema_obj = item_schema_data
|
|
239
|
+
else:
|
|
240
|
+
# Should not be hit if serialized with to_dict, but handle for robustness.
|
|
241
|
+
array_schema_obj = item_schema_data
|
|
252
242
|
|
|
243
|
+
object_schema_obj = None
|
|
244
|
+
if "object_schema" in param_data and param_data["object_schema"] is not None:
|
|
245
|
+
object_schema_obj = ParameterSchema.from_dict(param_data["object_schema"])
|
|
246
|
+
|
|
253
247
|
param = ParameterDefinition(
|
|
254
248
|
name=param_data["name"],
|
|
255
249
|
param_type=param_type_enum,
|
|
@@ -260,15 +254,14 @@ class ParameterSchema:
|
|
|
260
254
|
min_value=param_data.get("min_value"),
|
|
261
255
|
max_value=param_data.get("max_value"),
|
|
262
256
|
pattern=param_data.get("pattern"),
|
|
263
|
-
array_item_schema=
|
|
264
|
-
object_schema=
|
|
257
|
+
array_item_schema=array_schema_obj,
|
|
258
|
+
object_schema=object_schema_obj
|
|
265
259
|
)
|
|
266
260
|
schema.add_parameter(param)
|
|
267
|
-
|
|
268
261
|
return schema
|
|
269
262
|
|
|
270
263
|
def __len__(self) -> int:
|
|
271
264
|
return len(self.parameters)
|
|
272
265
|
|
|
273
266
|
def __bool__(self) -> bool:
|
|
274
|
-
return
|
|
267
|
+
return bool(self.parameters)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/pydantic_to_parameter_schema.py
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Type, get_origin, get_args, Union, List, Dict
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
_PYDANTIC_TYPE_MAP = {
|
|
10
|
+
str: ParameterType.STRING,
|
|
11
|
+
int: ParameterType.INTEGER,
|
|
12
|
+
float: ParameterType.FLOAT,
|
|
13
|
+
bool: ParameterType.BOOLEAN,
|
|
14
|
+
dict: ParameterType.OBJECT,
|
|
15
|
+
list: ParameterType.ARRAY,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def pydantic_to_parameter_schema(pydantic_model: Type[BaseModel]) -> ParameterSchema:
|
|
19
|
+
"""
|
|
20
|
+
Converts a Pydantic BaseModel into an AutoByteUs ParameterSchema.
|
|
21
|
+
|
|
22
|
+
This function inspects the Pydantic model's fields and recursively builds a
|
|
23
|
+
corresponding ParameterSchema, handling nested models and lists of models.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
pydantic_model: The Pydantic model class to convert.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A fully constructed ParameterSchema object.
|
|
30
|
+
"""
|
|
31
|
+
schema = ParameterSchema()
|
|
32
|
+
required_fields = {name for name, field_info in pydantic_model.model_fields.items() if field_info.is_required()}
|
|
33
|
+
|
|
34
|
+
for field_name, field_info in pydantic_model.model_fields.items():
|
|
35
|
+
param_type = ParameterType.STRING # Default
|
|
36
|
+
object_schema = None
|
|
37
|
+
array_item_schema = None
|
|
38
|
+
|
|
39
|
+
# Determine if the type is Optional (e.g., Union[str, None])
|
|
40
|
+
is_optional = False
|
|
41
|
+
field_type = field_info.annotation
|
|
42
|
+
origin_type = get_origin(field_type)
|
|
43
|
+
|
|
44
|
+
if origin_type is Union:
|
|
45
|
+
union_args = get_args(field_type)
|
|
46
|
+
if type(None) in union_args:
|
|
47
|
+
is_optional = True
|
|
48
|
+
# Get the non-None type from the Union
|
|
49
|
+
non_none_args = [arg for arg in union_args if arg is not type(None)]
|
|
50
|
+
if len(non_none_args) == 1:
|
|
51
|
+
field_type = non_none_args[0]
|
|
52
|
+
origin_type = get_origin(field_type)
|
|
53
|
+
|
|
54
|
+
if origin_type is list or origin_type is List:
|
|
55
|
+
param_type = ParameterType.ARRAY
|
|
56
|
+
list_item_type = get_args(field_type)[0] if get_args(field_type) else any
|
|
57
|
+
if isinstance(list_item_type, type) and issubclass(list_item_type, BaseModel):
|
|
58
|
+
# FIX: Recursively call the converter for the nested Pydantic model.
|
|
59
|
+
array_item_schema = pydantic_to_parameter_schema(list_item_type)
|
|
60
|
+
else:
|
|
61
|
+
# Fallback for list of primitives
|
|
62
|
+
primitive_type_str = _PYDANTIC_TYPE_MAP.get(list_item_type, ParameterType.STRING).value
|
|
63
|
+
array_item_schema = {"type": primitive_type_str}
|
|
64
|
+
|
|
65
|
+
elif isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
|
66
|
+
param_type = ParameterType.OBJECT
|
|
67
|
+
# Recursively convert the nested Pydantic model
|
|
68
|
+
object_schema = pydantic_to_parameter_schema(field_type)
|
|
69
|
+
else:
|
|
70
|
+
param_type = _PYDANTIC_TYPE_MAP.get(field_type, ParameterType.STRING)
|
|
71
|
+
|
|
72
|
+
schema.add_parameter(ParameterDefinition(
|
|
73
|
+
name=field_name,
|
|
74
|
+
param_type=param_type,
|
|
75
|
+
description=field_info.description or f"Parameter '{field_name}'.",
|
|
76
|
+
required=field_name in required_fields and not is_optional,
|
|
77
|
+
object_schema=object_schema,
|
|
78
|
+
array_item_schema=array_item_schema
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
return schema
|