schemez 1.2.4__py3-none-any.whl → 1.4.4__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.
Potentially problematic release.
This version of schemez might be problematic. Click here for more details.
- schemez/__init__.py +4 -0
- schemez/code_generation/__init__.py +6 -0
- schemez/code_generation/namespace_callable.py +76 -0
- schemez/code_generation/route_helpers.py +126 -0
- schemez/code_generation/tool_code_generator.py +300 -0
- schemez/code_generation/toolset_code_generator.py +167 -0
- schemez/executable.py +11 -9
- schemez/functionschema.py +13 -31
- schemez/schema.py +126 -78
- schemez/tool_executor/executor.py +1 -1
- {schemez-1.2.4.dist-info → schemez-1.4.4.dist-info}/METADATA +1 -1
- {schemez-1.2.4.dist-info → schemez-1.4.4.dist-info}/RECORD +14 -9
- {schemez-1.2.4.dist-info → schemez-1.4.4.dist-info}/WHEEL +1 -1
- {schemez-1.2.4.dist-info → schemez-1.4.4.dist-info}/licenses/LICENSE +0 -0
schemez/__init__.py
CHANGED
|
@@ -35,9 +35,11 @@ from schemez.schema_generators import (
|
|
|
35
35
|
create_constructor_schema,
|
|
36
36
|
)
|
|
37
37
|
from schemez.typedefs import OpenAIFunctionDefinition, OpenAIFunctionTool
|
|
38
|
+
from schemez.code_generation import ToolCodeGenerator, ToolsetCodeGenerator
|
|
38
39
|
|
|
39
40
|
__version__ = version("schemez")
|
|
40
41
|
|
|
42
|
+
|
|
41
43
|
__all__ = [
|
|
42
44
|
"ExecutableFunction",
|
|
43
45
|
"FunctionType",
|
|
@@ -54,6 +56,8 @@ __all__ = [
|
|
|
54
56
|
"SchemaDef",
|
|
55
57
|
"SchemaField",
|
|
56
58
|
"TOMLCode",
|
|
59
|
+
"ToolCodeGenerator",
|
|
60
|
+
"ToolsetCodeGenerator",
|
|
57
61
|
"YAMLCode",
|
|
58
62
|
"__version__",
|
|
59
63
|
"create_constructor_schema",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Meta-resource provider that exposes tools through Python execution."""
|
|
2
|
+
|
|
3
|
+
from schemez.code_generation.tool_code_generator import ToolCodeGenerator
|
|
4
|
+
from schemez.code_generation.toolset_code_generator import ToolsetCodeGenerator
|
|
5
|
+
|
|
6
|
+
__all__ = ["ToolCodeGenerator", "ToolsetCodeGenerator"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Namespace callable wrapper for tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from schemez.code_generation.tool_code_generator import ToolCodeGenerator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class NamespaceCallable:
|
|
18
|
+
"""Wrapper for tool functions with proper repr and call interface."""
|
|
19
|
+
|
|
20
|
+
callable: Callable
|
|
21
|
+
"""The callable function to execute."""
|
|
22
|
+
|
|
23
|
+
name_override: str | None = None
|
|
24
|
+
"""Override name for the callable, defaults to callable.__name__."""
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
"""Set function attributes for introspection."""
|
|
28
|
+
self.__name__ = self.name_override or self.callable.__name__
|
|
29
|
+
self.__doc__ = self.callable.__doc__ or ""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
"""Get the effective name of the callable."""
|
|
34
|
+
return self.name_override or self.callable.__name__
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_generator(cls, generator: ToolCodeGenerator) -> NamespaceCallable:
|
|
38
|
+
"""Create a NamespaceCallable from a ToolCodeGenerator.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
generator: The generator to wrap
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
NamespaceCallable instance
|
|
45
|
+
"""
|
|
46
|
+
return cls(generator.callable, generator.name_override)
|
|
47
|
+
|
|
48
|
+
async def __call__(self, *args, **kwargs) -> Any:
|
|
49
|
+
"""Execute the wrapped callable."""
|
|
50
|
+
try:
|
|
51
|
+
if inspect.iscoroutinefunction(self.callable):
|
|
52
|
+
result = await self.callable(*args, **kwargs)
|
|
53
|
+
|
|
54
|
+
result = self.callable(*args, **kwargs)
|
|
55
|
+
except Exception as e: # noqa: BLE001
|
|
56
|
+
return f"Error executing {self.name}: {e!s}"
|
|
57
|
+
else:
|
|
58
|
+
return result if result is not None else "Operation completed successfully"
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
"""Return detailed representation for debugging."""
|
|
62
|
+
return f"NamespaceCallable(name='{self.name}')"
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
"""Return readable string representation."""
|
|
66
|
+
return f"<tool: {self.name}>"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def signature(self) -> str:
|
|
70
|
+
"""Get function signature for debugging."""
|
|
71
|
+
try:
|
|
72
|
+
sig = inspect.signature(self.callable)
|
|
73
|
+
except (ValueError, TypeError):
|
|
74
|
+
return f"{self.name}(...)"
|
|
75
|
+
else:
|
|
76
|
+
return f"{self.name}{sig}"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Helper functions for FastAPI route generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from schemez.schema import json_schema_to_base_model
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from pydantic.fields import FieldInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_param_model(parameters_schema: dict[str, Any]) -> type[BaseModel] | None:
|
|
18
|
+
"""Create Pydantic model for parameter validation using schemez.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
parameters_schema: JSON schema for tool parameters
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Pydantic model class or None if no parameters
|
|
25
|
+
"""
|
|
26
|
+
if parameters_schema.get("properties"):
|
|
27
|
+
return json_schema_to_base_model(parameters_schema) # type: ignore
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_func_code(model_fields: dict[str, FieldInfo]) -> str:
|
|
32
|
+
"""Generate dynamic function code for FastAPI route handler.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
model_fields: Model fields from Pydantic model
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Generated function code as string
|
|
39
|
+
"""
|
|
40
|
+
route_params = []
|
|
41
|
+
for name, field_info in model_fields.items():
|
|
42
|
+
field_type = field_info.annotation
|
|
43
|
+
if field_info.is_required():
|
|
44
|
+
route_params.append(f"{name}: {field_type.__name__}") # type: ignore
|
|
45
|
+
else:
|
|
46
|
+
route_params.append(f"{name}: {field_type.__name__} = None") # type: ignore
|
|
47
|
+
|
|
48
|
+
# Create function signature dynamically
|
|
49
|
+
param_str = ", ".join(route_params)
|
|
50
|
+
return f"""
|
|
51
|
+
async def dynamic_handler({param_str}) -> dict[str, Any]:
|
|
52
|
+
kwargs = {{{", ".join(f'"{name}": {name}' for name in model_fields)}}}
|
|
53
|
+
return await route_handler(**kwargs)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_route_handler(tool_callable: Callable, param_cls: type | None) -> Callable:
|
|
58
|
+
"""Create FastAPI route handler for a tool.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
tool_callable: The tool function to execute
|
|
62
|
+
param_cls: Pydantic model for parameter validation
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Async route handler function
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
async def route_handler(*args, **kwargs) -> Any:
|
|
69
|
+
"""Route handler for the tool."""
|
|
70
|
+
if param_cls:
|
|
71
|
+
params_instance = param_cls(**kwargs) # Parse and validate parameters
|
|
72
|
+
dct = params_instance.model_dump() # Convert to dict and remove None values
|
|
73
|
+
clean_params = {k: v for k, v in dct.items() if v is not None}
|
|
74
|
+
result = await _execute_tool_function(tool_callable, **clean_params)
|
|
75
|
+
else:
|
|
76
|
+
result = await _execute_tool_function(tool_callable)
|
|
77
|
+
return {"result": result}
|
|
78
|
+
|
|
79
|
+
return route_handler
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def _execute_tool_function(tool_callable: Callable, **kwargs) -> Any:
|
|
83
|
+
"""Execute a tool function with the given parameters.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tool_callable: Tool function to execute
|
|
87
|
+
**kwargs: Tool parameters
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tool execution result
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# For now, just simulate execution
|
|
94
|
+
# In real implementation, this would call the actual tool
|
|
95
|
+
# potentially through sandbox providers
|
|
96
|
+
return f"Executed {tool_callable.__name__} with params: {kwargs}"
|
|
97
|
+
except Exception as e: # noqa: BLE001
|
|
98
|
+
return f"Error executing {tool_callable.__name__}: {e!s}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
from llmling_agent.tools.base import Tool
|
|
103
|
+
|
|
104
|
+
def greet(name: str, greeting: str = "Hello") -> str:
|
|
105
|
+
"""Greet someone."""
|
|
106
|
+
return f"{greeting}, {name}!"
|
|
107
|
+
|
|
108
|
+
# Create a tool and demonstrate helper functions
|
|
109
|
+
tool = Tool.from_callable(greet)
|
|
110
|
+
schema = tool.schema["function"]
|
|
111
|
+
parameters_schema = schema.get("parameters", {})
|
|
112
|
+
|
|
113
|
+
# Create parameter model
|
|
114
|
+
param_cls = create_param_model(dict(parameters_schema))
|
|
115
|
+
print(f"Generated parameter model: {param_cls}")
|
|
116
|
+
|
|
117
|
+
if param_cls:
|
|
118
|
+
print(f"Model fields: {param_cls.model_fields}")
|
|
119
|
+
|
|
120
|
+
# Generate function code
|
|
121
|
+
func_code = generate_func_code(param_cls.model_fields)
|
|
122
|
+
print(f"Generated function code:\n{func_code}")
|
|
123
|
+
|
|
124
|
+
# Create route handler
|
|
125
|
+
handler = create_route_handler(greet, param_cls)
|
|
126
|
+
print(f"Generated route handler: {handler}")
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Meta-resource provider that exposes tools through Python execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import TYPE_CHECKING, Any, get_origin
|
|
8
|
+
|
|
9
|
+
from schemez import create_schema
|
|
10
|
+
from schemez.code_generation.route_helpers import (
|
|
11
|
+
create_param_model,
|
|
12
|
+
create_route_handler,
|
|
13
|
+
generate_func_code,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
from fastapi import FastAPI
|
|
21
|
+
|
|
22
|
+
from schemez.typedefs import OpenAIFunctionTool, Property
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TYPE_MAP = {
|
|
26
|
+
"string": "str",
|
|
27
|
+
"integer": "int",
|
|
28
|
+
"number": "float",
|
|
29
|
+
"boolean": "bool",
|
|
30
|
+
"array": "list",
|
|
31
|
+
"null": "None",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ToolCodeGenerator:
|
|
37
|
+
"""Generates code artifacts for a single tool."""
|
|
38
|
+
|
|
39
|
+
callable: Callable
|
|
40
|
+
"""Tool to generate code for."""
|
|
41
|
+
|
|
42
|
+
schema: OpenAIFunctionTool
|
|
43
|
+
"""Schema of the tool."""
|
|
44
|
+
|
|
45
|
+
name_override: str | None = None
|
|
46
|
+
"""Name override for the function to generate code for."""
|
|
47
|
+
|
|
48
|
+
exclude_types: list[type] = field(default_factory=list)
|
|
49
|
+
"""Exclude parameters from generated code (like context types)."""
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_callable(
|
|
53
|
+
cls,
|
|
54
|
+
fn: Callable,
|
|
55
|
+
exclude_types: list[type] | None = None,
|
|
56
|
+
) -> ToolCodeGenerator:
|
|
57
|
+
"""Create a ToolCodeGenerator from a Tool."""
|
|
58
|
+
schema = create_schema(fn).model_dump_openai()
|
|
59
|
+
schema["function"]["name"] = fn.__name__
|
|
60
|
+
schema["function"]["description"] = fn.__doc__ or ""
|
|
61
|
+
return cls(schema=schema, callable=fn, exclude_types=exclude_types or [])
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def name(self) -> str:
|
|
65
|
+
"""Name of the tool."""
|
|
66
|
+
return self.name_override or self.callable.__name__
|
|
67
|
+
|
|
68
|
+
def _extract_basic_signature(self, return_type: str = "Any") -> str:
|
|
69
|
+
"""Fallback signature extraction from tool schema."""
|
|
70
|
+
schema = self.schema["function"]
|
|
71
|
+
params = schema.get("parameters", {}).get("properties", {})
|
|
72
|
+
required = set(schema.get("parameters", {}).get("required", []))
|
|
73
|
+
|
|
74
|
+
param_strs = []
|
|
75
|
+
for name, param_info in params.items():
|
|
76
|
+
# Skip context parameters that should be hidden from users
|
|
77
|
+
if self._is_context_parameter(name):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
type_hint = self._infer_parameter_type(name, param_info)
|
|
81
|
+
|
|
82
|
+
if name not in required:
|
|
83
|
+
# Check for actual default value in schema
|
|
84
|
+
default_value = param_info.get("default")
|
|
85
|
+
if default_value is not None:
|
|
86
|
+
if isinstance(default_value, str):
|
|
87
|
+
param_strs.append(f"{name}: {type_hint} = {default_value!r}")
|
|
88
|
+
else:
|
|
89
|
+
param_strs.append(f"{name}: {type_hint} = {default_value}")
|
|
90
|
+
else:
|
|
91
|
+
param_strs.append(f"{name}: {type_hint} = None")
|
|
92
|
+
else:
|
|
93
|
+
param_strs.append(f"{name}: {type_hint}")
|
|
94
|
+
|
|
95
|
+
return f"{self.name}({', '.join(param_strs)}) -> {return_type}"
|
|
96
|
+
|
|
97
|
+
def _infer_parameter_type(self, param_name: str, param_info: Property) -> str:
|
|
98
|
+
"""Infer parameter type from schema and function inspection."""
|
|
99
|
+
schema_type = param_info.get("type", "Any")
|
|
100
|
+
|
|
101
|
+
# If schema has a specific type, use it
|
|
102
|
+
if schema_type != "object":
|
|
103
|
+
return TYPE_MAP.get(schema_type, "Any")
|
|
104
|
+
|
|
105
|
+
# For 'object' type, try to infer from function signature
|
|
106
|
+
try:
|
|
107
|
+
callable_func = self.callable
|
|
108
|
+
# Use wrapped signature if available (for context parameter hiding)
|
|
109
|
+
sig = getattr(callable_func, "__signature__", None) or inspect.signature(
|
|
110
|
+
callable_func
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if param_name in sig.parameters:
|
|
114
|
+
param = sig.parameters[param_name]
|
|
115
|
+
|
|
116
|
+
# Try annotation first
|
|
117
|
+
if param.annotation != inspect.Parameter.empty:
|
|
118
|
+
if hasattr(param.annotation, "__name__"):
|
|
119
|
+
return param.annotation.__name__
|
|
120
|
+
return str(param.annotation)
|
|
121
|
+
|
|
122
|
+
# Infer from default value
|
|
123
|
+
if param.default != inspect.Parameter.empty:
|
|
124
|
+
default_type = type(param.default).__name__
|
|
125
|
+
# Map common types
|
|
126
|
+
if default_type in ["int", "float", "str", "bool"]:
|
|
127
|
+
return default_type
|
|
128
|
+
# If no default and it's required, assume str for web-like functions
|
|
129
|
+
required = set(
|
|
130
|
+
self.schema.get("function", {})
|
|
131
|
+
.get("parameters", {})
|
|
132
|
+
.get("required", [])
|
|
133
|
+
)
|
|
134
|
+
if param_name in required:
|
|
135
|
+
return "str"
|
|
136
|
+
|
|
137
|
+
except Exception: # noqa: BLE001
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Fallback to Any for unresolved object types
|
|
141
|
+
return "Any"
|
|
142
|
+
|
|
143
|
+
def _get_return_model_name(self) -> str:
|
|
144
|
+
"""Get the return model name for a tool."""
|
|
145
|
+
try:
|
|
146
|
+
schema = create_schema(self.callable)
|
|
147
|
+
if schema.returns.get("type") == "object":
|
|
148
|
+
return f"{self.name.title()}Response"
|
|
149
|
+
if schema.returns.get("type") == "array":
|
|
150
|
+
return f"list[{self.name.title()}Item]"
|
|
151
|
+
return TYPE_MAP.get(schema.returns.get("type", "string"), "Any")
|
|
152
|
+
except Exception: # noqa: BLE001
|
|
153
|
+
return "Any"
|
|
154
|
+
|
|
155
|
+
def get_function_signature(self) -> str:
|
|
156
|
+
"""Extract function signature using schemez."""
|
|
157
|
+
try:
|
|
158
|
+
return_model_name = self._get_return_model_name()
|
|
159
|
+
return self._extract_basic_signature(return_model_name)
|
|
160
|
+
except Exception: # noqa: BLE001
|
|
161
|
+
return self._extract_basic_signature("Any")
|
|
162
|
+
|
|
163
|
+
def _get_callable_signature(self) -> inspect.Signature:
|
|
164
|
+
"""Get signature from callable, respecting wrapped signatures."""
|
|
165
|
+
# Use wrapped signature if available (for context parameter hiding)
|
|
166
|
+
return getattr(self.callable, "__signature__", None) or inspect.signature(
|
|
167
|
+
self.callable
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _is_context_parameter(self, param_name: str) -> bool:
|
|
171
|
+
"""Check if a parameter is a context parameter that should be hidden."""
|
|
172
|
+
try:
|
|
173
|
+
sig = self._get_callable_signature()
|
|
174
|
+
if param_name not in sig.parameters:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
param = sig.parameters[param_name]
|
|
178
|
+
if param.annotation == inspect.Parameter.empty:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
annotation = param.annotation
|
|
182
|
+
|
|
183
|
+
for typ in self.exclude_types:
|
|
184
|
+
if self._types_match(annotation, typ):
|
|
185
|
+
return True
|
|
186
|
+
except Exception: # noqa: BLE001
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _types_match(self, annotation: Any, exclude_type: type) -> bool:
|
|
192
|
+
"""Check if annotation matches exclude_type using various strategies."""
|
|
193
|
+
try:
|
|
194
|
+
# Direct type match
|
|
195
|
+
if annotation is exclude_type:
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
# Handle generic types - get origin for comparison
|
|
199
|
+
origin_annotation = get_origin(annotation)
|
|
200
|
+
if origin_annotation is exclude_type:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
# String-based comparison for forward references and __future__.annotations
|
|
204
|
+
annotation_str = str(annotation)
|
|
205
|
+
exclude_type_name = exclude_type.__name__
|
|
206
|
+
exclude_type_full_name = f"{exclude_type.__module__}.{exclude_type.__name__}"
|
|
207
|
+
|
|
208
|
+
# Check various string representations
|
|
209
|
+
if (
|
|
210
|
+
exclude_type_name in annotation_str
|
|
211
|
+
or exclude_type_full_name in annotation_str
|
|
212
|
+
):
|
|
213
|
+
# Be more specific to avoid false positives
|
|
214
|
+
# Check if it's the exact type name, not just a substring
|
|
215
|
+
import re
|
|
216
|
+
|
|
217
|
+
patterns = [
|
|
218
|
+
rf"\b{re.escape(exclude_type_name)}\b",
|
|
219
|
+
rf"\b{re.escape(exclude_type_full_name)}\b",
|
|
220
|
+
]
|
|
221
|
+
if any(re.search(pattern, annotation_str) for pattern in patterns):
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
except Exception: # noqa: BLE001
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def generate_return_model(self) -> str | None:
|
|
230
|
+
"""Generate Pydantic model code for the tool's return type."""
|
|
231
|
+
try:
|
|
232
|
+
schema = create_schema(self.callable)
|
|
233
|
+
if schema.returns.get("type") not in {"object", "array"}:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
class_name = f"{self.name.title()}Response"
|
|
237
|
+
model_code = schema.to_pydantic_model_code(class_name=class_name)
|
|
238
|
+
return model_code.strip() or None
|
|
239
|
+
|
|
240
|
+
except Exception: # noqa: BLE001
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Route generation methods
|
|
244
|
+
def generate_route_handler(self) -> Callable:
|
|
245
|
+
"""Generate FastAPI route handler for this tool.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Async route handler function
|
|
249
|
+
"""
|
|
250
|
+
# Extract parameter schema
|
|
251
|
+
schema = self.schema["function"]
|
|
252
|
+
parameters_schema = schema.get("parameters", {})
|
|
253
|
+
|
|
254
|
+
# Create parameter model
|
|
255
|
+
param_cls = create_param_model(dict(parameters_schema))
|
|
256
|
+
|
|
257
|
+
# Create route handler
|
|
258
|
+
return create_route_handler(self.callable, param_cls)
|
|
259
|
+
|
|
260
|
+
def add_route_to_app(self, app: FastAPI, path_prefix: str = "/tools") -> None:
|
|
261
|
+
"""Add this tool's route to FastAPI app.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
app: FastAPI application instance
|
|
265
|
+
path_prefix: Path prefix for the route
|
|
266
|
+
"""
|
|
267
|
+
# Extract parameter schema
|
|
268
|
+
schema = self.schema["function"]
|
|
269
|
+
parameters_schema = schema.get("parameters", {})
|
|
270
|
+
|
|
271
|
+
# Create parameter model
|
|
272
|
+
param_cls = create_param_model(dict(parameters_schema))
|
|
273
|
+
|
|
274
|
+
# Create the route handler
|
|
275
|
+
route_handler = self.generate_route_handler()
|
|
276
|
+
|
|
277
|
+
# Set up the route with proper parameter annotations for FastAPI
|
|
278
|
+
if param_cls:
|
|
279
|
+
# Get field information from the generated model
|
|
280
|
+
model_fields = param_cls.model_fields
|
|
281
|
+
func_code = generate_func_code(model_fields)
|
|
282
|
+
# Execute the dynamic function creation
|
|
283
|
+
namespace = {"route_handler": route_handler, "Any": Any}
|
|
284
|
+
exec(func_code, namespace)
|
|
285
|
+
dynamic_handler: Callable = namespace["dynamic_handler"] # type: ignore
|
|
286
|
+
else:
|
|
287
|
+
|
|
288
|
+
async def dynamic_handler() -> dict[str, Any]:
|
|
289
|
+
return await route_handler()
|
|
290
|
+
|
|
291
|
+
# Add route to FastAPI app
|
|
292
|
+
app.get(f"{path_prefix}/{self.name}")(dynamic_handler)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
import webbrowser
|
|
297
|
+
|
|
298
|
+
generator = ToolCodeGenerator.from_callable(webbrowser.open)
|
|
299
|
+
sig = generator.get_function_signature()
|
|
300
|
+
print(sig)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Orchestrates code generation for multiple tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import inspect
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from schemez.code_generation.namespace_callable import NamespaceCallable
|
|
11
|
+
from schemez.code_generation.tool_code_generator import ToolCodeGenerator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
USAGE = """\
|
|
21
|
+
Usage notes:
|
|
22
|
+
- Write your code inside an 'async def main():' function
|
|
23
|
+
- All tool functions are async, use 'await'
|
|
24
|
+
- Use 'return' statements to return values from main()
|
|
25
|
+
- Generated model classes are available for type checking
|
|
26
|
+
- Use 'await report_progress(current, total, message)' for long-running operations
|
|
27
|
+
- DO NOT call asyncio.run() or try to run the main function yourself
|
|
28
|
+
- DO NOT import asyncio or other modules - tools are already available
|
|
29
|
+
- Example:
|
|
30
|
+
async def main():
|
|
31
|
+
for i in range(5):
|
|
32
|
+
await report_progress(i, 5, f'Step {i+1} for {name}')
|
|
33
|
+
should_continue = await ask_user('Continue?', 'bool')
|
|
34
|
+
if not should_continue:
|
|
35
|
+
break
|
|
36
|
+
return f'Completed for {name}'
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ToolsetCodeGenerator:
|
|
43
|
+
"""Generates code artifacts for multiple tools."""
|
|
44
|
+
|
|
45
|
+
generators: Sequence[ToolCodeGenerator]
|
|
46
|
+
"""ToolCodeGenerator instances for each tool."""
|
|
47
|
+
|
|
48
|
+
include_signatures: bool = True
|
|
49
|
+
"""Include function signatures in documentation."""
|
|
50
|
+
|
|
51
|
+
include_docstrings: bool = True
|
|
52
|
+
"""Include function docstrings in documentation."""
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_callables(
|
|
56
|
+
cls,
|
|
57
|
+
callables: Sequence[Callable],
|
|
58
|
+
include_signatures: bool = True,
|
|
59
|
+
include_docstrings: bool = True,
|
|
60
|
+
exclude_types: list[type] | None = None,
|
|
61
|
+
) -> ToolsetCodeGenerator:
|
|
62
|
+
"""Create a ToolsetCodeGenerator from a sequence of Tools.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
callables: Callables to generate code for
|
|
66
|
+
include_signatures: Include function signatures in documentation
|
|
67
|
+
include_docstrings: Include function docstrings in documentation
|
|
68
|
+
exclude_types: Parameter Types to exclude from the generated code
|
|
69
|
+
Often used for context parameters.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ToolsetCodeGenerator instance
|
|
73
|
+
"""
|
|
74
|
+
generators = [
|
|
75
|
+
ToolCodeGenerator.from_callable(i, exclude_types=exclude_types)
|
|
76
|
+
for i in callables
|
|
77
|
+
]
|
|
78
|
+
return cls(generators, include_signatures, include_docstrings)
|
|
79
|
+
|
|
80
|
+
def generate_tool_description(self) -> str:
|
|
81
|
+
"""Generate comprehensive tool description with available functions."""
|
|
82
|
+
if not self.generators:
|
|
83
|
+
return "Execute Python code (no tools available)"
|
|
84
|
+
|
|
85
|
+
return_models = self.generate_return_models()
|
|
86
|
+
parts = [
|
|
87
|
+
"Execute Python code with the following tools available as async functions:",
|
|
88
|
+
"",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
if return_models:
|
|
92
|
+
parts.extend([
|
|
93
|
+
"# Generated return type models",
|
|
94
|
+
return_models,
|
|
95
|
+
"",
|
|
96
|
+
"# Available functions:",
|
|
97
|
+
"",
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
for generator in self.generators:
|
|
101
|
+
if self.include_signatures:
|
|
102
|
+
signature = generator.get_function_signature()
|
|
103
|
+
parts.append(f"async def {signature}:")
|
|
104
|
+
else:
|
|
105
|
+
parts.append(f"async def {generator.name}(...):")
|
|
106
|
+
|
|
107
|
+
if self.include_docstrings and generator.callable.__doc__:
|
|
108
|
+
indented_desc = " " + generator.callable.__doc__.replace(
|
|
109
|
+
"\n", "\n "
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Add warning for async functions without proper return type hints
|
|
113
|
+
if inspect.iscoroutinefunction(generator.callable):
|
|
114
|
+
sig = inspect.signature(generator.callable)
|
|
115
|
+
if sig.return_annotation == inspect.Signature.empty:
|
|
116
|
+
indented_desc += "\n \n Note: This async function should explicitly return a value." # noqa: E501
|
|
117
|
+
|
|
118
|
+
parts.append(f' """{indented_desc}"""')
|
|
119
|
+
parts.append("")
|
|
120
|
+
|
|
121
|
+
parts.append(USAGE)
|
|
122
|
+
|
|
123
|
+
return "\n".join(parts)
|
|
124
|
+
|
|
125
|
+
def generate_execution_namespace(self) -> dict[str, Any]:
|
|
126
|
+
"""Build Python namespace with tool functions and generated models."""
|
|
127
|
+
namespace: dict[str, Any] = {"__builtins__": __builtins__, "_result": None}
|
|
128
|
+
|
|
129
|
+
# Add tool functions
|
|
130
|
+
for generator in self.generators:
|
|
131
|
+
namespace[generator.name] = NamespaceCallable.from_generator(generator)
|
|
132
|
+
|
|
133
|
+
# Add generated model classes to namespace
|
|
134
|
+
if models_code := self.generate_return_models():
|
|
135
|
+
with contextlib.suppress(Exception):
|
|
136
|
+
exec(models_code, namespace)
|
|
137
|
+
|
|
138
|
+
return namespace
|
|
139
|
+
|
|
140
|
+
def generate_return_models(self) -> str:
|
|
141
|
+
"""Generate Pydantic models for tool return types."""
|
|
142
|
+
model_parts = [
|
|
143
|
+
code for g in self.generators if (code := g.generate_return_model())
|
|
144
|
+
]
|
|
145
|
+
return "\n\n".join(model_parts) if model_parts else ""
|
|
146
|
+
|
|
147
|
+
def add_all_routes(self, app: FastAPI, path_prefix: str = "/tools") -> None:
|
|
148
|
+
"""Add FastAPI routes for all tools.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
app: FastAPI application instance
|
|
152
|
+
path_prefix: Path prefix for routes
|
|
153
|
+
"""
|
|
154
|
+
for generator in self.generators:
|
|
155
|
+
generator.add_route_to_app(app, path_prefix)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
|
|
160
|
+
async def no_annotations_func(test):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
generator = ToolsetCodeGenerator.from_callables([no_annotations_func])
|
|
164
|
+
models = generator.generate_return_models()
|
|
165
|
+
print(models)
|
|
166
|
+
namespace = generator.generate_execution_namespace()
|
|
167
|
+
print(namespace)
|
schemez/executable.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import AsyncIterator, Callable # noqa: TC003
|
|
5
|
-
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar, assert_never, overload
|
|
6
6
|
|
|
7
7
|
from schemez.functionschema import FunctionType, create_schema
|
|
8
8
|
|
|
@@ -34,8 +34,11 @@ class ExecutableFunction[T_co]:
|
|
|
34
34
|
schema: OpenAI function schema
|
|
35
35
|
func: The actual function to execute
|
|
36
36
|
"""
|
|
37
|
+
from schemez.functionschema import _determine_function_type
|
|
38
|
+
|
|
37
39
|
self.schema = schema
|
|
38
40
|
self.func = func
|
|
41
|
+
self.function_type = _determine_function_type(self.func)
|
|
39
42
|
|
|
40
43
|
def run(self, *args: Any, **kwargs: Any) -> T_co | list[T_co]: # noqa: PLR0911
|
|
41
44
|
"""Run the function synchronously.
|
|
@@ -47,7 +50,7 @@ class ExecutableFunction[T_co]:
|
|
|
47
50
|
Returns:
|
|
48
51
|
Either a single result or list of results for generators
|
|
49
52
|
"""
|
|
50
|
-
match self.
|
|
53
|
+
match self.function_type:
|
|
51
54
|
case FunctionType.SYNC:
|
|
52
55
|
return self.func(*args, **kwargs) # type: ignore
|
|
53
56
|
case FunctionType.ASYNC:
|
|
@@ -86,9 +89,8 @@ class ExecutableFunction[T_co]:
|
|
|
86
89
|
return loop.run_until_complete(
|
|
87
90
|
self._collect_async_gen(*args, **kwargs),
|
|
88
91
|
)
|
|
89
|
-
case _:
|
|
90
|
-
|
|
91
|
-
raise ValueError(msg)
|
|
92
|
+
case _ as unreachable:
|
|
93
|
+
assert_never(unreachable)
|
|
92
94
|
|
|
93
95
|
async def _collect_async_gen(self, *args: Any, **kwargs: Any) -> list[T_co]:
|
|
94
96
|
"""Collect async generator results into a list.
|
|
@@ -115,7 +117,7 @@ class ExecutableFunction[T_co]:
|
|
|
115
117
|
Raises:
|
|
116
118
|
ValueError: If the function type is unknown
|
|
117
119
|
"""
|
|
118
|
-
match self.
|
|
120
|
+
match self.function_type:
|
|
119
121
|
case FunctionType.SYNC:
|
|
120
122
|
return self.func(*args, **kwargs) # type: ignore
|
|
121
123
|
case FunctionType.ASYNC:
|
|
@@ -125,7 +127,7 @@ class ExecutableFunction[T_co]:
|
|
|
125
127
|
case FunctionType.ASYNC_GENERATOR:
|
|
126
128
|
return [x async for x in self.func(*args, **kwargs)] # type: ignore
|
|
127
129
|
case _:
|
|
128
|
-
msg = f"Unknown function type: {self.
|
|
130
|
+
msg = f"Unknown function type: {self.function_type}"
|
|
129
131
|
raise ValueError(msg)
|
|
130
132
|
|
|
131
133
|
async def astream(self, *args: Any, **kwargs: Any) -> AsyncIterator[T_co]:
|
|
@@ -141,7 +143,7 @@ class ExecutableFunction[T_co]:
|
|
|
141
143
|
Raises:
|
|
142
144
|
ValueError: If the function type is unknown
|
|
143
145
|
"""
|
|
144
|
-
match self.
|
|
146
|
+
match self.function_type:
|
|
145
147
|
case FunctionType.SYNC_GENERATOR:
|
|
146
148
|
for x in self.func(*args, **kwargs): # type: ignore
|
|
147
149
|
yield x
|
|
@@ -153,7 +155,7 @@ class ExecutableFunction[T_co]:
|
|
|
153
155
|
case FunctionType.ASYNC:
|
|
154
156
|
yield await self.func(*args, **kwargs) # type: ignore
|
|
155
157
|
case _:
|
|
156
|
-
msg = f"Unknown function type: {self.
|
|
158
|
+
msg = f"Unknown function type: {self.function_type}"
|
|
157
159
|
raise ValueError(msg)
|
|
158
160
|
|
|
159
161
|
|
schemez/functionschema.py
CHANGED
|
@@ -23,11 +23,7 @@ import docstring_parser
|
|
|
23
23
|
import pydantic
|
|
24
24
|
|
|
25
25
|
from schemez import log
|
|
26
|
-
from schemez.typedefs import
|
|
27
|
-
OpenAIFunctionDefinition,
|
|
28
|
-
OpenAIFunctionTool,
|
|
29
|
-
ToolParameters,
|
|
30
|
-
)
|
|
26
|
+
from schemez.typedefs import OpenAIFunctionDefinition, OpenAIFunctionTool, ToolParameters
|
|
31
27
|
|
|
32
28
|
|
|
33
29
|
if typing.TYPE_CHECKING:
|
|
@@ -77,38 +73,22 @@ class FunctionSchema(pydantic.BaseModel):
|
|
|
77
73
|
"""The name of the function as it will be presented to the OpenAI API."""
|
|
78
74
|
|
|
79
75
|
description: str | None = None
|
|
80
|
-
"""
|
|
81
|
-
Optional description of what the function does. This helps the AI understand
|
|
82
|
-
when and how to use the function.
|
|
83
|
-
"""
|
|
76
|
+
"""Optional description of what the function does."""
|
|
84
77
|
|
|
85
78
|
parameters: ToolParameters = pydantic.Field(
|
|
86
79
|
default_factory=lambda: ToolParameters(type="object", properties={}),
|
|
87
80
|
)
|
|
88
|
-
"""
|
|
89
|
-
JSON Schema object describing the function's parameters. Contains type information,
|
|
90
|
-
descriptions, and constraints for each parameter.
|
|
91
|
-
"""
|
|
81
|
+
"""JSON Schema object describing the function's parameters."""
|
|
92
82
|
|
|
93
83
|
required: list[str] = pydantic.Field(default_factory=list)
|
|
94
84
|
"""
|
|
95
85
|
List of parameter names that are required (do not have default values).
|
|
96
|
-
These parameters must be provided when calling the function.
|
|
97
86
|
"""
|
|
98
87
|
|
|
99
88
|
returns: dict[str, Any] = pydantic.Field(
|
|
100
89
|
default_factory=lambda: {"type": "object"},
|
|
101
90
|
)
|
|
102
|
-
"""
|
|
103
|
-
JSON Schema object describing the function's return type. Used for type checking
|
|
104
|
-
and documentation purposes.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
function_type: FunctionType = FunctionType.SYNC
|
|
108
|
-
"""
|
|
109
|
-
The execution pattern of the function (sync, async, generator, or async generator).
|
|
110
|
-
Used to determine how to properly invoke the function.
|
|
111
|
-
"""
|
|
91
|
+
"""JSON Schema object describing the function's return type."""
|
|
112
92
|
|
|
113
93
|
model_config = pydantic.ConfigDict(frozen=True)
|
|
114
94
|
|
|
@@ -377,7 +357,6 @@ class FunctionSchema(pydantic.BaseModel):
|
|
|
377
357
|
parameters=parameters,
|
|
378
358
|
required=required,
|
|
379
359
|
returns={"type": "object"},
|
|
380
|
-
function_type=FunctionType.SYNC,
|
|
381
360
|
)
|
|
382
361
|
|
|
383
362
|
|
|
@@ -647,22 +626,26 @@ def _determine_function_type(func: Callable[..., Any]) -> FunctionType:
|
|
|
647
626
|
def create_schema(
|
|
648
627
|
func: Callable[..., Any],
|
|
649
628
|
name_override: str | None = None,
|
|
629
|
+
description_override: str | None = None,
|
|
650
630
|
) -> FunctionSchema:
|
|
651
631
|
"""Create an OpenAI function schema from a Python function.
|
|
652
632
|
|
|
633
|
+
If an iterator is passed, the schema return type is a list of the iterator's
|
|
634
|
+
element type.
|
|
635
|
+
Variable arguments (*args) and keyword arguments (**kwargs) are not
|
|
636
|
+
supported in OpenAI function schemas and will be ignored with a warning.
|
|
637
|
+
|
|
653
638
|
Args:
|
|
654
639
|
func: Function to create schema for
|
|
655
640
|
name_override: Optional name override (otherwise the function name)
|
|
641
|
+
description_override: Optional description override
|
|
642
|
+
(otherwise the function docstring)
|
|
656
643
|
|
|
657
644
|
Returns:
|
|
658
645
|
Schema representing the function
|
|
659
646
|
|
|
660
647
|
Raises:
|
|
661
648
|
TypeError: If input is not callable
|
|
662
|
-
|
|
663
|
-
Note:
|
|
664
|
-
Variable arguments (*args) and keyword arguments (**kwargs) are not
|
|
665
|
-
supported in OpenAI function schemas and will be ignored with a warning.
|
|
666
649
|
"""
|
|
667
650
|
if not callable(func):
|
|
668
651
|
msg = f"Expected callable, got {type(func)}"
|
|
@@ -737,11 +720,10 @@ def create_schema(
|
|
|
737
720
|
|
|
738
721
|
return FunctionSchema(
|
|
739
722
|
name=name_override or getattr(func, "__name__", "unknown") or "unknown",
|
|
740
|
-
description=docstring.short_description,
|
|
723
|
+
description=description_override or docstring.short_description,
|
|
741
724
|
parameters=parameters, # Now includes required fields
|
|
742
725
|
required=required,
|
|
743
726
|
returns=returns_dct,
|
|
744
|
-
function_type=function_type,
|
|
745
727
|
)
|
|
746
728
|
|
|
747
729
|
|
schemez/schema.py
CHANGED
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Self
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
-
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
9
9
|
import upath
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Callable
|
|
14
14
|
|
|
15
|
-
from llmling_agent.agent.agent import AgentType
|
|
16
15
|
from llmling_agent.models.content import BaseContent
|
|
17
16
|
from upath.types import JoinablePathLike
|
|
18
17
|
|
|
@@ -69,41 +68,47 @@ class Schema(BaseModel):
|
|
|
69
68
|
return get_function_model(func, name=name)
|
|
70
69
|
|
|
71
70
|
@classmethod
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
file_content: bytes,
|
|
75
|
-
source_type: SourceType = "pdf",
|
|
76
|
-
model: str = "google-gla:gemini-2.0-flash",
|
|
77
|
-
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
|
78
|
-
user_prompt: str = DEFAULT_USER_PROMPT,
|
|
79
|
-
provider: AgentType = "pydantic_ai",
|
|
80
|
-
) -> Self:
|
|
81
|
-
"""Create a schema model from a document using AI.
|
|
71
|
+
def from_json_schema(cls, json_schema: dict[str, Any]) -> type[Schema]:
|
|
72
|
+
"""Create a schema model from a JSON schema.
|
|
82
73
|
|
|
83
74
|
Args:
|
|
84
|
-
|
|
85
|
-
source_type: The type of the document
|
|
86
|
-
model: The AI model to use for schema extraction
|
|
87
|
-
system_prompt: The system prompt to use for schema extraction
|
|
88
|
-
user_prompt: The user prompt to use for schema extraction
|
|
89
|
-
provider: The provider to use for schema extraction
|
|
75
|
+
json_schema: The JSON schema to create a schema from
|
|
90
76
|
|
|
91
77
|
Returns:
|
|
92
|
-
A new schema model class based on the
|
|
78
|
+
A new schema model class based on the JSON schema
|
|
93
79
|
"""
|
|
94
|
-
from
|
|
80
|
+
from datamodel_code_generator import DataModelType, PythonVersion
|
|
81
|
+
from datamodel_code_generator.model import get_data_model_types
|
|
82
|
+
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
|
|
83
|
+
from pydantic.v1.main import ModelMetaclass
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
data_model_types = get_data_model_types(
|
|
86
|
+
DataModelType.PydanticBaseModel, target_python_version=PythonVersion.PY_312
|
|
87
|
+
)
|
|
88
|
+
parser = JsonSchemaParser(
|
|
89
|
+
f"""{json_schema}""",
|
|
90
|
+
data_model_type=data_model_types.data_model,
|
|
91
|
+
data_model_root_type=data_model_types.root_model,
|
|
92
|
+
data_model_field_type=data_model_types.field_model,
|
|
93
|
+
data_type_manager_type=data_model_types.data_type_manager,
|
|
94
|
+
dump_resolve_reference_action=data_model_types.dump_resolve_reference_action,
|
|
95
|
+
)
|
|
96
|
+
code_str = (
|
|
97
|
+
parser.parse()
|
|
98
|
+
.replace("from pydantic ", "from pydantic.v1 ") # type: ignore
|
|
99
|
+
.replace("from __future__ import annotations\n\n", "")
|
|
100
|
+
)
|
|
101
|
+
ex_namespace: dict[str, Any] = {}
|
|
102
|
+
exec(code_str, ex_namespace, ex_namespace)
|
|
103
|
+
model = None
|
|
104
|
+
for v in ex_namespace.values():
|
|
105
|
+
if isinstance(v, ModelMetaclass):
|
|
106
|
+
model = v
|
|
107
|
+
if not model:
|
|
108
|
+
msg = "Class not found in output"
|
|
109
|
+
raise Exception(msg) # noqa: TRY002
|
|
110
|
+
model.__module__ = __name__
|
|
111
|
+
return model # type: ignore
|
|
107
112
|
|
|
108
113
|
@classmethod
|
|
109
114
|
async def from_vision_llm(
|
|
@@ -113,7 +118,6 @@ class Schema(BaseModel):
|
|
|
113
118
|
model: str = "google-gla:gemini-2.0-flash",
|
|
114
119
|
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
|
115
120
|
user_prompt: str = DEFAULT_USER_PROMPT,
|
|
116
|
-
provider: AgentType = "pydantic_ai",
|
|
117
121
|
) -> Self:
|
|
118
122
|
"""Create a schema model from a document using AI.
|
|
119
123
|
|
|
@@ -123,7 +127,6 @@ class Schema(BaseModel):
|
|
|
123
127
|
model: The AI model to use for schema extraction
|
|
124
128
|
system_prompt: The system prompt to use for schema extraction
|
|
125
129
|
user_prompt: The user prompt to use for schema extraction
|
|
126
|
-
provider: The provider to use for schema extraction
|
|
127
130
|
|
|
128
131
|
Returns:
|
|
129
132
|
A new schema model class based on the document
|
|
@@ -134,45 +137,11 @@ class Schema(BaseModel):
|
|
|
134
137
|
content: BaseContent = PDFBase64Content.from_bytes(file_content)
|
|
135
138
|
else:
|
|
136
139
|
content = ImageBase64Content.from_bytes(file_content)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
system_prompt=system_prompt.format(name=cls.__name__),
|
|
140
|
-
provider=provider,
|
|
141
|
-
).to_structured(cls)
|
|
140
|
+
prompt = system_prompt.format(name=cls.__name__)
|
|
141
|
+
agent = Agent(model=model, system_prompt=prompt, output_type=cls)
|
|
142
142
|
chat_message = await agent.run(user_prompt, content)
|
|
143
143
|
return chat_message.content
|
|
144
144
|
|
|
145
|
-
@classmethod
|
|
146
|
-
def from_llm_sync(
|
|
147
|
-
cls,
|
|
148
|
-
text: str,
|
|
149
|
-
model: str = "google-gla:gemini-2.0-flash",
|
|
150
|
-
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
|
151
|
-
user_prompt: str = DEFAULT_USER_PROMPT,
|
|
152
|
-
provider: AgentType = "pydantic_ai",
|
|
153
|
-
) -> Self:
|
|
154
|
-
"""Create a schema model from a text snippet using AI.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
text: The text to create a schema from
|
|
158
|
-
model: The AI model to use for schema extraction
|
|
159
|
-
system_prompt: The system prompt to use for schema extraction
|
|
160
|
-
user_prompt: The user prompt to use for schema extraction
|
|
161
|
-
provider: The provider to use for schema extraction
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
A new schema model class based on the document
|
|
165
|
-
"""
|
|
166
|
-
from llmling_agent import Agent
|
|
167
|
-
|
|
168
|
-
agent = Agent[None]( # type:ignore[var-annotated]
|
|
169
|
-
model=model,
|
|
170
|
-
system_prompt=system_prompt.format(name=cls.__name__),
|
|
171
|
-
provider=provider,
|
|
172
|
-
).to_structured(cls)
|
|
173
|
-
chat_message = anyenv.run_sync(agent.run(user_prompt, text))
|
|
174
|
-
return chat_message.content
|
|
175
|
-
|
|
176
145
|
@classmethod
|
|
177
146
|
async def from_llm(
|
|
178
147
|
cls,
|
|
@@ -180,7 +149,6 @@ class Schema(BaseModel):
|
|
|
180
149
|
model: str = "google-gla:gemini-2.0-flash",
|
|
181
150
|
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
|
182
151
|
user_prompt: str = DEFAULT_USER_PROMPT,
|
|
183
|
-
provider: AgentType = "pydantic_ai",
|
|
184
152
|
) -> Self:
|
|
185
153
|
"""Create a schema model from a text snippet using AI.
|
|
186
154
|
|
|
@@ -189,18 +157,14 @@ class Schema(BaseModel):
|
|
|
189
157
|
model: The AI model to use for schema extraction
|
|
190
158
|
system_prompt: The system prompt to use for schema extraction
|
|
191
159
|
user_prompt: The user prompt to use for schema extraction
|
|
192
|
-
provider: The provider to use for schema extraction
|
|
193
160
|
|
|
194
161
|
Returns:
|
|
195
162
|
A new schema model class based on the document
|
|
196
163
|
"""
|
|
197
164
|
from llmling_agent import Agent
|
|
198
165
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
system_prompt=system_prompt.format(name=cls.__name__),
|
|
202
|
-
provider=provider,
|
|
203
|
-
).to_structured(cls)
|
|
166
|
+
prompt = system_prompt.format(name=cls.__name__)
|
|
167
|
+
agent = Agent(model=model, system_prompt=prompt, output_type=cls)
|
|
204
168
|
chat_message = await agent.run(user_prompt, text)
|
|
205
169
|
return chat_message.content
|
|
206
170
|
|
|
@@ -280,3 +244,87 @@ class Schema(BaseModel):
|
|
|
280
244
|
class_name=class_name,
|
|
281
245
|
target_python_version=target_python_version,
|
|
282
246
|
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def json_schema_to_base_model(
|
|
250
|
+
schema: dict[str, Any], model_cls: type[BaseModel] = Schema
|
|
251
|
+
) -> type[Schema]:
|
|
252
|
+
type_mapping: dict[str, type] = {
|
|
253
|
+
"string": str,
|
|
254
|
+
"integer": int,
|
|
255
|
+
"number": float,
|
|
256
|
+
"boolean": bool,
|
|
257
|
+
"array": list,
|
|
258
|
+
"object": dict,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
properties = schema.get("properties", {})
|
|
262
|
+
required_fields = schema.get("required", [])
|
|
263
|
+
model_fields = {}
|
|
264
|
+
|
|
265
|
+
def process_field(field_name: str, field_props: dict[str, Any]) -> tuple:
|
|
266
|
+
"""Recursively processes a field and returns its type and Field instance."""
|
|
267
|
+
json_type = field_props.get("type", "string")
|
|
268
|
+
enum_values = field_props.get("enum")
|
|
269
|
+
|
|
270
|
+
# Handle Enums
|
|
271
|
+
if enum_values:
|
|
272
|
+
enum_name: str = f"{field_name.capitalize()}Enum"
|
|
273
|
+
field_type: Any = Enum(enum_name, {v: v for v in enum_values})
|
|
274
|
+
# Handle Nested Objects
|
|
275
|
+
elif json_type == "object" and "properties" in field_props:
|
|
276
|
+
# Recursively create submodel
|
|
277
|
+
field_type = json_schema_to_base_model(field_props) # type: ignore
|
|
278
|
+
# Handle Arrays with Nested Objects
|
|
279
|
+
elif json_type == "array" and "items" in field_props:
|
|
280
|
+
item_props = field_props["items"]
|
|
281
|
+
if item_props.get("type") == "object":
|
|
282
|
+
item_type: Any = json_schema_to_base_model(item_props) # pyright: ignore[reportRedeclaration]
|
|
283
|
+
else:
|
|
284
|
+
item_type = type_mapping.get(item_props.get("type"), Any) # pyright: ignore[reportAssignmentType]
|
|
285
|
+
field_type = list[item_type] # type: ignore
|
|
286
|
+
else:
|
|
287
|
+
field_type = type_mapping.get(json_type, Any) # type: ignore
|
|
288
|
+
|
|
289
|
+
# Handle default values and optionality
|
|
290
|
+
default_value = field_props.get("default", ...)
|
|
291
|
+
nullable = field_props.get("nullable", False)
|
|
292
|
+
description = field_props.get("title", "")
|
|
293
|
+
|
|
294
|
+
if nullable:
|
|
295
|
+
field_type = Optional[field_type] # type: ignore # noqa: UP045
|
|
296
|
+
|
|
297
|
+
if field_name not in required_fields:
|
|
298
|
+
default_value = field_props.get("default")
|
|
299
|
+
|
|
300
|
+
return field_type, Field(default_value, description=description)
|
|
301
|
+
|
|
302
|
+
# Process each field
|
|
303
|
+
for field_name, field_props in properties.items():
|
|
304
|
+
model_fields[field_name] = process_field(field_name, field_props)
|
|
305
|
+
|
|
306
|
+
return create_model( # type: ignore
|
|
307
|
+
schema.get("title", "DynamicModel"), **model_fields, __base__=model_cls
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
schema = {
|
|
313
|
+
"$id": "https://example.com/person.schema.json",
|
|
314
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
315
|
+
"title": "Person",
|
|
316
|
+
"type": "object",
|
|
317
|
+
"properties": {
|
|
318
|
+
"firstName": {"type": "string", "description": "The person's first name."},
|
|
319
|
+
"lastName": {"type": "string", "description": "The person's last name."},
|
|
320
|
+
"age": {
|
|
321
|
+
"description": "Age in years, must be equal to or greater than zero.",
|
|
322
|
+
"type": "integer",
|
|
323
|
+
"minimum": 0,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
model = Schema.from_json_schema(schema)
|
|
328
|
+
import devtools
|
|
329
|
+
|
|
330
|
+
devtools.debug(model.__fields__)
|
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
schemez/__init__.py,sha256=
|
|
1
|
+
schemez/__init__.py,sha256=UwLAA0B5whh63w9sYQvs_b_NNm6TBCm86-zPQiDxj3g,1799
|
|
2
2
|
schemez/bind_kwargs.py,sha256=ChyArgNa5R8VdwSJmmrQItMH9Ld6hStWBISw-T1wyws,6228
|
|
3
3
|
schemez/code.py,sha256=usZLov9i5KpK1W2VJxngUzeetgrINtodiooG_AxN-y4,2072
|
|
4
|
+
schemez/code_generation/__init__.py,sha256=KV2ETgN8sKHlAOGnktURAHDJbz8jImBNputaxhdlin8,286
|
|
5
|
+
schemez/code_generation/namespace_callable.py,sha256=LiQHsS3J46snBj4uhKNrI-dboeQNPO2QwdbhSk3-gyE,2382
|
|
6
|
+
schemez/code_generation/route_helpers.py,sha256=YZfD0BUZ6_0iLnSzf2vKa8Fu2kJNfC-8oYzaQ--x8fA,4056
|
|
7
|
+
schemez/code_generation/tool_code_generator.py,sha256=6j6F6dDhXruInaZeu3ReK0x7wA56oAGRfzRCytnlaQk,10766
|
|
8
|
+
schemez/code_generation/toolset_code_generator.py,sha256=0L3Wrqc4XUbtbptHFLWARogBWMy50iVpQzzvYQlzNe0,5806
|
|
4
9
|
schemez/convert.py,sha256=3sOxOgDaFzV7uiOUSM6_Sy0YlafIlZRSevs5y2vT1Kw,4403
|
|
5
10
|
schemez/create_type.py,sha256=wrdqdzXtfxZfsgp9IroldoYGTJs_Rdli8TiscqhV2bI,11647
|
|
6
11
|
schemez/docstrings.py,sha256=kmd660wcomXzKac0SSNYxPRNbVCUovrpmE9jwnVRS6c,4115
|
|
7
|
-
schemez/executable.py,sha256=
|
|
8
|
-
schemez/functionschema.py,sha256=
|
|
12
|
+
schemez/executable.py,sha256=ZvZJdUMI_EWjTqp-wUTzob3-cZDPmHgA03W756gSzKc,7190
|
|
13
|
+
schemez/functionschema.py,sha256=LCNHn4ZCitUIoiEylfI59i0Em5TmnJ5N0rfxUWWlE38,25803
|
|
9
14
|
schemez/helpers.py,sha256=YFx7UKYDI_sn5sVGAzzl5rcB26iBvLatvZlEFsQM5rc,7874
|
|
10
15
|
schemez/log.py,sha256=i0SDbIfWmuC_nfJdQOYAdUYaR0TBk3Mhu-K3M-lnAM4,364
|
|
11
16
|
schemez/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
17
|
schemez/pydantic_types.py,sha256=8vgSl8i2z9n0fB-8AJj-D3TBByEWE5IxItBxQ0XwXFI,1640
|
|
13
|
-
schemez/schema.py,sha256=
|
|
18
|
+
schemez/schema.py,sha256=OUiMNkoJZzsRQ2hTdKLvHFrmeoSZZhtbgnV4EjeIPA4,11552
|
|
14
19
|
schemez/schema_generators.py,sha256=Gze7S7dQkTsl_1ckeHLXPxx4jQo7RB6hHQM-5fpAsrA,6973
|
|
15
20
|
schemez/schemadef/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
21
|
schemez/schemadef/schemadef.py,sha256=FtD7TOnYxiuYOIfadRHKkkbZn98mWFb0_lKfPsPR-hI,14393
|
|
17
22
|
schemez/tool_executor/__init__.py,sha256=7wLjhA1NGekTMsiIfWLAv6J7qYhWDlanH9DKU4v1c6c,263
|
|
18
|
-
schemez/tool_executor/executor.py,sha256
|
|
23
|
+
schemez/tool_executor/executor.py,sha256=0VOY9N4Epqdv_MYU-BoDiTKbFiyIwGk_pfe5JcxJq4o,10497
|
|
19
24
|
schemez/tool_executor/helpers.py,sha256=zxfI9tUkUx8Dy9fNP89-2kqfV8eZwQ3re2Gmd-oekb0,1476
|
|
20
25
|
schemez/tool_executor/types.py,sha256=l2DxUIEHP9bjLnEaXZ6X428cSviicTDJsc3wfSNqKxg,675
|
|
21
26
|
schemez/typedefs.py,sha256=3OAUQ1nin9nlsOcTPAO5xrsOqVUfwsH_7_cexQYREus,6091
|
|
22
|
-
schemez-1.
|
|
23
|
-
schemez-1.
|
|
24
|
-
schemez-1.
|
|
25
|
-
schemez-1.
|
|
27
|
+
schemez-1.4.4.dist-info/licenses/LICENSE,sha256=AteGCH9r177TxxrOFEiOARrastASsf7yW6MQxlAHdwA,1078
|
|
28
|
+
schemez-1.4.4.dist-info/WHEEL,sha256=DpNsHFUm_gffZe1FgzmqwuqiuPC6Y-uBCzibcJcdupM,78
|
|
29
|
+
schemez-1.4.4.dist-info/METADATA,sha256=3iBXsicqp4iXZ3nwDFvwHU_akqL17f0_zt5jqyCbNOE,11616
|
|
30
|
+
schemez-1.4.4.dist-info/RECORD,,
|
|
File without changes
|