unique_toolkit 0.8.39__py3-none-any.whl → 0.8.40__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.
- unique_toolkit/_common/base_model_type_attribute.py +303 -0
- {unique_toolkit-0.8.39.dist-info → unique_toolkit-0.8.40.dist-info}/METADATA +5 -1
- {unique_toolkit-0.8.39.dist-info → unique_toolkit-0.8.40.dist-info}/RECORD +5 -4
- {unique_toolkit-0.8.39.dist-info → unique_toolkit-0.8.40.dist-info}/LICENSE +0 -0
- {unique_toolkit-0.8.39.dist-info → unique_toolkit-0.8.40.dist-info}/WHEEL +0 -0
@@ -0,0 +1,303 @@
|
|
1
|
+
"""
|
2
|
+
The following can be used to define a pydantic BaseModel that has has
|
3
|
+
an attribute of type Pydantic BaseModel.
|
4
|
+
|
5
|
+
This is useful for:
|
6
|
+
- Tooldefinition for large language models (LLMs) with flexible parameters.
|
7
|
+
- General Endpoint defintions from configuration
|
8
|
+
"""
|
9
|
+
|
10
|
+
import json
|
11
|
+
from enum import StrEnum
|
12
|
+
from typing import Annotated, Any, TypeVar, Union, get_args, get_origin
|
13
|
+
|
14
|
+
from jambo import SchemaConverter
|
15
|
+
from jambo.types.json_schema_type import JSONSchema
|
16
|
+
from pydantic import (
|
17
|
+
BaseModel,
|
18
|
+
BeforeValidator,
|
19
|
+
Field,
|
20
|
+
create_model,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
def _get_actual_type(python_type: type) -> type | None | Any:
|
25
|
+
if get_origin(python_type) is not None:
|
26
|
+
origin = get_origin(python_type)
|
27
|
+
args = get_args(python_type)
|
28
|
+
|
29
|
+
if origin is Annotated:
|
30
|
+
# For Annotated types, the first argument is the actual type
|
31
|
+
if args:
|
32
|
+
actual_type = args[0]
|
33
|
+
# Recursively handle nested generic types (e.g., Annotated[Optional[str], ...])
|
34
|
+
if get_origin(actual_type) is not None:
|
35
|
+
return _get_actual_type(actual_type)
|
36
|
+
else:
|
37
|
+
raise ValueError(f"Invalid Annotated type: {python_type}")
|
38
|
+
elif origin is Union:
|
39
|
+
# For Union types (including Optional), use the first non-None type
|
40
|
+
if args:
|
41
|
+
for arg in args:
|
42
|
+
if arg is not type(None): # Skip NoneType
|
43
|
+
return _get_actual_type(arg)
|
44
|
+
raise ValueError(f"Union type contains only None: {python_type}")
|
45
|
+
else:
|
46
|
+
raise ValueError(f"Invalid Union type: {python_type}")
|
47
|
+
else:
|
48
|
+
# Other generic types, use the origin
|
49
|
+
actual_type = origin
|
50
|
+
else:
|
51
|
+
# Regular type
|
52
|
+
actual_type = python_type
|
53
|
+
|
54
|
+
return actual_type
|
55
|
+
|
56
|
+
|
57
|
+
class ParameterType(StrEnum):
|
58
|
+
STRING = "string"
|
59
|
+
INTEGER = "integer"
|
60
|
+
NUMBER = "number"
|
61
|
+
BOOLEAN = "boolean"
|
62
|
+
|
63
|
+
def to_python_type(self) -> type:
|
64
|
+
"""Convert ParameterType to Python type"""
|
65
|
+
|
66
|
+
match self:
|
67
|
+
case ParameterType.STRING:
|
68
|
+
return str
|
69
|
+
case ParameterType.INTEGER:
|
70
|
+
return int
|
71
|
+
case ParameterType.NUMBER:
|
72
|
+
return float
|
73
|
+
case ParameterType.BOOLEAN:
|
74
|
+
return bool
|
75
|
+
case _:
|
76
|
+
raise ValueError(f"Invalid ParameterType: {self}")
|
77
|
+
|
78
|
+
@classmethod
|
79
|
+
def from_python_type(cls, python_type: type) -> "ParameterType":
|
80
|
+
type_to_check = _get_actual_type(python_type)
|
81
|
+
|
82
|
+
# Ensure we have a class before calling issubclass
|
83
|
+
if not isinstance(type_to_check, type):
|
84
|
+
raise ValueError(f"Invalid Python type: {python_type}")
|
85
|
+
|
86
|
+
# Check bool first since bool is a subclass of int in Python
|
87
|
+
if issubclass(type_to_check, bool):
|
88
|
+
return cls.BOOLEAN
|
89
|
+
if issubclass(type_to_check, int):
|
90
|
+
return cls.INTEGER
|
91
|
+
if issubclass(type_to_check, float):
|
92
|
+
return cls.NUMBER
|
93
|
+
if issubclass(type_to_check, str):
|
94
|
+
return cls.STRING
|
95
|
+
raise ValueError(f"Invalid Python type: {python_type}")
|
96
|
+
|
97
|
+
|
98
|
+
class Parameter(BaseModel):
|
99
|
+
type: ParameterType
|
100
|
+
name: str
|
101
|
+
description: str
|
102
|
+
required: bool
|
103
|
+
|
104
|
+
|
105
|
+
def create_pydantic_model_from_parameter_list(
|
106
|
+
title: str, parameter_list: list[Parameter]
|
107
|
+
) -> type[BaseModel]:
|
108
|
+
"""Create a Pydantic model from MCP tool's input schema"""
|
109
|
+
|
110
|
+
# Convert JSON schema properties to Pydantic fields
|
111
|
+
fields = {}
|
112
|
+
for parameter in parameter_list:
|
113
|
+
if parameter.required:
|
114
|
+
field = Field(description=parameter.description)
|
115
|
+
else:
|
116
|
+
field = Field(default=None, description=parameter.description)
|
117
|
+
|
118
|
+
fields[parameter.name] = (
|
119
|
+
parameter.type.to_python_type(),
|
120
|
+
field,
|
121
|
+
)
|
122
|
+
|
123
|
+
return create_model(title, **fields)
|
124
|
+
|
125
|
+
|
126
|
+
def convert_to_base_model_type(
|
127
|
+
value: type[BaseModel] | str | list[Parameter] | None,
|
128
|
+
) -> type[BaseModel]:
|
129
|
+
"""
|
130
|
+
BeforeValidator that ensures the final type is always of type[BaseModel].
|
131
|
+
|
132
|
+
If the input is already a BaseModel class, returns it as-is.
|
133
|
+
If the input is a list of Parameter as defined above, converts it to a BaseModel class
|
134
|
+
If the input is a str (JSON schema), converts it to a BaseModel class using SchemaConverter from Jambo.
|
135
|
+
"""
|
136
|
+
if isinstance(value, type) and issubclass(value, BaseModel):
|
137
|
+
return value
|
138
|
+
|
139
|
+
if isinstance(value, list):
|
140
|
+
if all(isinstance(item, Parameter) for item in value):
|
141
|
+
return create_pydantic_model_from_parameter_list("Parameters", value)
|
142
|
+
|
143
|
+
converter = SchemaConverter()
|
144
|
+
if isinstance(value, str):
|
145
|
+
return converter.build(JSONSchema(**json.loads(value)))
|
146
|
+
|
147
|
+
raise ValueError(f"Invalid value: {value}")
|
148
|
+
|
149
|
+
|
150
|
+
def base_model_to_parameter_list(model: type[BaseModel]) -> list[Parameter]:
|
151
|
+
parameter = []
|
152
|
+
for field_name, field_info in model.model_fields.items():
|
153
|
+
parameter.append(
|
154
|
+
Parameter(
|
155
|
+
type=ParameterType.from_python_type(field_info.annotation or str),
|
156
|
+
name=field_name,
|
157
|
+
description=field_info.description or "",
|
158
|
+
required=field_info.is_required(),
|
159
|
+
)
|
160
|
+
)
|
161
|
+
return parameter
|
162
|
+
|
163
|
+
|
164
|
+
# Create the annotated type that ensures BaseModel and generates clean JSON schema
|
165
|
+
|
166
|
+
TModel = TypeVar("TModel", bound=BaseModel)
|
167
|
+
|
168
|
+
|
169
|
+
class BaseModelTypeTitle(StrEnum):
|
170
|
+
LIST_OF_PARAMETERS = "List of Parameters"
|
171
|
+
JSON_SCHEMA_AS_STRING = "JSON Schema as String"
|
172
|
+
USE_MODEL_FROM_CODE = "Use Model from Code"
|
173
|
+
|
174
|
+
|
175
|
+
ListOfParameters = Annotated[
|
176
|
+
list[Parameter], Field(title=BaseModelTypeTitle.LIST_OF_PARAMETERS.value)
|
177
|
+
]
|
178
|
+
JSONSchemaString = Annotated[
|
179
|
+
str, Field(title=BaseModelTypeTitle.JSON_SCHEMA_AS_STRING.value)
|
180
|
+
]
|
181
|
+
CodefinedModelType = Annotated[
|
182
|
+
None, Field(title=BaseModelTypeTitle.USE_MODEL_FROM_CODE.value)
|
183
|
+
]
|
184
|
+
|
185
|
+
|
186
|
+
BaseModelType = Annotated[
|
187
|
+
type[TModel],
|
188
|
+
BeforeValidator(
|
189
|
+
convert_to_base_model_type,
|
190
|
+
json_schema_input_type=ListOfParameters | JSONSchemaString | CodefinedModelType,
|
191
|
+
),
|
192
|
+
]
|
193
|
+
|
194
|
+
|
195
|
+
def get_json_schema_extra_for_base_model_type(model: type[BaseModel]):
|
196
|
+
"""
|
197
|
+
Returns a json_schema_extra mutator that injects defaults
|
198
|
+
into both the 'string' and 'list[Parameter]' branches.
|
199
|
+
|
200
|
+
This is used to define default for the "oneOf"/"anyOf" validation
|
201
|
+
of the parameters attribute.
|
202
|
+
"""
|
203
|
+
sample_params = base_model_to_parameter_list(model)
|
204
|
+
|
205
|
+
def _mutate(schema: dict) -> None:
|
206
|
+
json_default = json.dumps(model.model_json_schema())
|
207
|
+
params_default = [p.model_dump() for p in sample_params]
|
208
|
+
|
209
|
+
for key in ("oneOf", "anyOf"):
|
210
|
+
if key in schema:
|
211
|
+
for entry in schema[key]:
|
212
|
+
if (
|
213
|
+
entry.get("type") == "string"
|
214
|
+
and entry.get("title")
|
215
|
+
== BaseModelTypeTitle.JSON_SCHEMA_AS_STRING.value
|
216
|
+
):
|
217
|
+
entry["default"] = json_default
|
218
|
+
if (
|
219
|
+
entry.get("type") == "array"
|
220
|
+
and entry.get("title")
|
221
|
+
== BaseModelTypeTitle.LIST_OF_PARAMETERS.value
|
222
|
+
):
|
223
|
+
entry["default"] = params_default
|
224
|
+
|
225
|
+
return _mutate
|
226
|
+
|
227
|
+
|
228
|
+
if __name__ == "__main__":
|
229
|
+
import json
|
230
|
+
from pathlib import Path
|
231
|
+
from typing import Generic
|
232
|
+
|
233
|
+
from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam
|
234
|
+
from openai.types.shared_params.function_definition import FunctionDefinition
|
235
|
+
from pydantic import BaseModel, Field, field_serializer
|
236
|
+
|
237
|
+
class ToolDescription(BaseModel, Generic[TModel]):
|
238
|
+
name: str = Field(
|
239
|
+
...,
|
240
|
+
pattern=r"^[a-zA-Z1-9_-]+$",
|
241
|
+
description="Name must adhere to the pattern ^[a-zA-Z1-9_-]+$",
|
242
|
+
)
|
243
|
+
description: str = Field(
|
244
|
+
...,
|
245
|
+
description="Description of what the tool is doing the tool",
|
246
|
+
)
|
247
|
+
|
248
|
+
strict: bool = Field(
|
249
|
+
default=False,
|
250
|
+
description="Setting strict to true will ensure function calls reliably adhere to the function schema, instead of being best effort.",
|
251
|
+
)
|
252
|
+
|
253
|
+
parameters: BaseModelType[TModel] = Field(
|
254
|
+
...,
|
255
|
+
description="Json Schema for the tool parameters. Must be valid JSON Schema and able to convert to a Pydantic model",
|
256
|
+
)
|
257
|
+
|
258
|
+
@field_serializer("parameters")
|
259
|
+
def serialize_parameters(self, parameters: type[BaseModel]):
|
260
|
+
return parameters.model_json_schema()
|
261
|
+
|
262
|
+
def to_openai(self) -> ChatCompletionToolParam:
|
263
|
+
return ChatCompletionToolParam(
|
264
|
+
function=FunctionDefinition(
|
265
|
+
name=self.name,
|
266
|
+
description=self.description,
|
267
|
+
parameters=self.parameters.model_json_schema(),
|
268
|
+
strict=self.strict,
|
269
|
+
),
|
270
|
+
type="function",
|
271
|
+
)
|
272
|
+
|
273
|
+
class WeatherToolParameterModel(BaseModel):
|
274
|
+
lon: float = Field(
|
275
|
+
..., description="The longitude of the location to get the weather for"
|
276
|
+
)
|
277
|
+
lat: float = Field(
|
278
|
+
..., description="The latitude of the location to get the weather for"
|
279
|
+
)
|
280
|
+
name: str = Field(
|
281
|
+
..., description="The name of the location to get the weather for"
|
282
|
+
)
|
283
|
+
|
284
|
+
class GetWeatherTool(ToolDescription[WeatherToolParameterModel]):
|
285
|
+
parameters: BaseModelType[WeatherToolParameterModel] = Field(
|
286
|
+
default=WeatherToolParameterModel,
|
287
|
+
json_schema_extra=get_json_schema_extra_for_base_model_type(
|
288
|
+
WeatherToolParameterModel
|
289
|
+
),
|
290
|
+
)
|
291
|
+
|
292
|
+
# The json schema can be used in the RSJF library to create a valid frontend component.
|
293
|
+
# You can test it on https://rjsf-team.github.io/react-jsonschema-form/
|
294
|
+
file = Path(__file__).parent / "weather_tool_schema.json"
|
295
|
+
with file.open("w") as f:
|
296
|
+
f.write(json.dumps(GetWeatherTool.model_json_schema(), indent=2))
|
297
|
+
|
298
|
+
# Notice that the t.parameters is a pydantic model with type annotations
|
299
|
+
t = GetWeatherTool(
|
300
|
+
name="GetWeather", description="Get the weather for a given location"
|
301
|
+
)
|
302
|
+
t.parameters(lon=100, lat=100, name="Test")
|
303
|
+
print(t.model_dump())
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: unique_toolkit
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.40
|
4
4
|
Summary:
|
5
5
|
License: Proprietary
|
6
6
|
Author: Martin Fadler
|
@@ -10,6 +10,7 @@ Classifier: License :: Other/Proprietary License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Requires-Dist: jambo (>=0.1.2,<0.2.0)
|
13
14
|
Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
14
15
|
Requires-Dist: openai (>=1.99.9,<2.0.0)
|
15
16
|
Requires-Dist: pillow (>=10.4.0,<11.0.0)
|
@@ -116,6 +117,9 @@ All notable changes to this project will be documented in this file.
|
|
116
117
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
117
118
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
118
119
|
|
120
|
+
## [0.8.40] - 2025-09-02
|
121
|
+
- Add frontend compatible type for pydantic BaseModel types in pydantic BaseModels
|
122
|
+
|
119
123
|
## [0.8.39] - 2025-09-02
|
120
124
|
- include `get_async_openai_client`
|
121
125
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
unique_toolkit/__init__.py,sha256=nbOYPIKERt-ITsgifrnJhatn1YNR38Ntumw-dCn_tsA,714
|
2
2
|
unique_toolkit/_common/_base_service.py,sha256=S8H0rAebx7GsOldA7xInLp3aQJt9yEPDQdsGSFRJsGg,276
|
3
3
|
unique_toolkit/_common/_time_utils.py,sha256=ztmTovTvr-3w71Ns2VwXC65OKUUh-sQlzbHdKTQWm-w,135
|
4
|
+
unique_toolkit/_common/base_model_type_attribute.py,sha256=7rzVqjXa0deYEixeo_pJSJcQ7nKXpWK_UGpOiEH3yZY,10382
|
4
5
|
unique_toolkit/_common/chunk_relevancy_sorter/config.py,sha256=v6Ljo-WIZCtYJgfaPfpzZegCV0DEw_nNhTzNtw0Jg7c,1744
|
5
6
|
unique_toolkit/_common/chunk_relevancy_sorter/exception.py,sha256=1mY4zjbvnXsd5oIxwiVsma09bS2XRnHrxW8KJBGtgCM,126
|
6
7
|
unique_toolkit/_common/chunk_relevancy_sorter/schemas.py,sha256=doAWPPx8d0zIqHMXmnJy47Z5_NlblJBhMqo8KE7fyyc,1329
|
@@ -130,7 +131,7 @@ unique_toolkit/tools/utils/execution/execution.py,sha256=vjG2Y6awsGNtlvyQAGCTthQ
|
|
130
131
|
unique_toolkit/tools/utils/source_handling/schema.py,sha256=vzAyf6ZWNexjMO0OrnB8y2glGkvAilmGGQXd6zcDaKw,870
|
131
132
|
unique_toolkit/tools/utils/source_handling/source_formatting.py,sha256=C7uayNbdkNVJdEARA5CENnHtNY1SU6etlaqbgHNyxaQ,9152
|
132
133
|
unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py,sha256=oM5ZxEgzROrnX1229KViCAFjRxl9wCTzWZoinYSHleM,6979
|
133
|
-
unique_toolkit-0.8.
|
134
|
-
unique_toolkit-0.8.
|
135
|
-
unique_toolkit-0.8.
|
136
|
-
unique_toolkit-0.8.
|
134
|
+
unique_toolkit-0.8.40.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
|
135
|
+
unique_toolkit-0.8.40.dist-info/METADATA,sha256=jLHRjA0QbBqoZ5p31Lo5eXqE87eDCCcJTJQJfetVXWk,30273
|
136
|
+
unique_toolkit-0.8.40.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
137
|
+
unique_toolkit-0.8.40.dist-info/RECORD,,
|
File without changes
|
File without changes
|