schemez 1.2.4__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of schemez might be problematic. Click here for more details.

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,272 @@
1
+ """Meta-resource provider that exposes tools through Python execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import inspect
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from pydantic_ai import RunContext
10
+
11
+ from schemez import create_schema
12
+ from schemez.code_generation.route_helpers import (
13
+ create_param_model,
14
+ create_route_handler,
15
+ generate_func_code,
16
+ )
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable
21
+
22
+ from fastapi import FastAPI
23
+
24
+ from schemez.typedefs import OpenAIFunctionTool, Property
25
+
26
+
27
+ TYPE_MAP = {
28
+ "string": "str",
29
+ "integer": "int",
30
+ "number": "float",
31
+ "boolean": "bool",
32
+ "array": "list",
33
+ "null": "None",
34
+ }
35
+
36
+
37
+ @dataclass
38
+ class ToolCodeGenerator:
39
+ """Generates code artifacts for a single tool."""
40
+
41
+ schema: OpenAIFunctionTool
42
+ """Schema of the tool."""
43
+
44
+ callable: Callable
45
+ """Tool to generate code for."""
46
+
47
+ name_override: str | None = None
48
+ """Name of the tool."""
49
+
50
+ @classmethod
51
+ def from_callable(cls, fn: Callable) -> ToolCodeGenerator:
52
+ """Create a ToolCodeGenerator from a Tool."""
53
+ schema = create_schema(fn).model_dump_openai()
54
+ schema["function"]["name"] = fn.__name__
55
+ schema["function"]["description"] = fn.__doc__ or ""
56
+ return cls(schema=schema, callable=callable)
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ """Name of the tool."""
61
+ return self.name_override or self.callable.__name__
62
+
63
+ def _extract_basic_signature(self, return_type: str = "Any") -> str:
64
+ """Fallback signature extraction from tool schema."""
65
+ schema = self.schema["function"]
66
+ params = schema.get("parameters", {}).get("properties", {})
67
+ required = set(schema.get("parameters", {}).get("required", []))
68
+
69
+ param_strs = []
70
+ for name, param_info in params.items():
71
+ # Skip context parameters that should be hidden from users
72
+ if self._is_context_parameter(name):
73
+ continue
74
+
75
+ type_hint = self._infer_parameter_type(name, param_info)
76
+
77
+ if name not in required:
78
+ # Check for actual default value in schema
79
+ default_value = param_info.get("default")
80
+ if default_value is not None:
81
+ if isinstance(default_value, str):
82
+ param_strs.append(f"{name}: {type_hint} = {default_value!r}")
83
+ else:
84
+ param_strs.append(f"{name}: {type_hint} = {default_value}")
85
+ else:
86
+ param_strs.append(f"{name}: {type_hint} = None")
87
+ else:
88
+ param_strs.append(f"{name}: {type_hint}")
89
+
90
+ return f"{self.name}({', '.join(param_strs)}) -> {return_type}"
91
+
92
+ def _infer_parameter_type(self, param_name: str, param_info: Property) -> str:
93
+ """Infer parameter type from schema and function inspection."""
94
+ schema_type = param_info.get("type", "Any")
95
+
96
+ # If schema has a specific type, use it
97
+ if schema_type != "object":
98
+ return TYPE_MAP.get(schema_type, "Any")
99
+
100
+ # For 'object' type, try to infer from function signature
101
+ try:
102
+ callable_func = self.callable
103
+ # Use wrapped signature if available (for context parameter hiding)
104
+ sig = getattr(callable_func, "__signature__", None) or inspect.signature(
105
+ callable_func
106
+ )
107
+
108
+ if param_name in sig.parameters:
109
+ param = sig.parameters[param_name]
110
+
111
+ # Try annotation first
112
+ if param.annotation != inspect.Parameter.empty:
113
+ if hasattr(param.annotation, "__name__"):
114
+ return param.annotation.__name__
115
+ return str(param.annotation)
116
+
117
+ # Infer from default value
118
+ if param.default != inspect.Parameter.empty:
119
+ default_type = type(param.default).__name__
120
+ # Map common types
121
+ if default_type in ["int", "float", "str", "bool"]:
122
+ return default_type
123
+ # If no default and it's required, assume str for web-like functions
124
+ required = set(
125
+ self.schema.get("function", {})
126
+ .get("parameters", {})
127
+ .get("required", [])
128
+ )
129
+ if param_name in required:
130
+ return "str"
131
+
132
+ except Exception: # noqa: BLE001
133
+ pass
134
+
135
+ # Fallback to Any for unresolved object types
136
+ return "Any"
137
+
138
+ def _get_return_model_name(self) -> str:
139
+ """Get the return model name for a tool."""
140
+ try:
141
+ schema = create_schema(self.callable)
142
+ if schema.returns.get("type") == "object":
143
+ return f"{self.name.title()}Response"
144
+ if schema.returns.get("type") == "array":
145
+ return f"list[{self.name.title()}Item]"
146
+ return TYPE_MAP.get(schema.returns.get("type", "string"), "Any")
147
+ except Exception: # noqa: BLE001
148
+ return "Any"
149
+
150
+ def get_function_signature(self) -> str:
151
+ """Extract function signature using schemez."""
152
+ try:
153
+ return_model_name = self._get_return_model_name()
154
+ return self._extract_basic_signature(return_model_name)
155
+ except Exception: # noqa: BLE001
156
+ return self._extract_basic_signature("Any")
157
+
158
+ def _get_callable_signature(self) -> inspect.Signature:
159
+ """Get signature from callable, respecting wrapped signatures."""
160
+ # Use wrapped signature if available (for context parameter hiding)
161
+ return getattr(self.callable, "__signature__", None) or inspect.signature(
162
+ self.callable
163
+ )
164
+
165
+ def _is_context_parameter(self, param_name: str) -> bool: # noqa: PLR0911
166
+ """Check if a parameter is a context parameter that should be hidden."""
167
+ try:
168
+ sig = self._get_callable_signature()
169
+ if param_name not in sig.parameters:
170
+ return False
171
+
172
+ param = sig.parameters[param_name]
173
+ if param.annotation == inspect.Parameter.empty:
174
+ return False
175
+
176
+ # Check if parameter is RunContext or AgentContext
177
+ annotation = param.annotation
178
+ annotation_str = str(annotation)
179
+
180
+ # Handle RunContext (including parameterized like RunContext[None])
181
+ if annotation is RunContext:
182
+ return True
183
+
184
+ # Check for parameterized RunContext using string matching
185
+ if "RunContext" in annotation_str:
186
+ return True
187
+
188
+ # Handle AgentContext
189
+ if hasattr(annotation, "__name__") and annotation.__name__ == "AgentContext":
190
+ return True
191
+
192
+ # Check for AgentContext in string representation
193
+ if "AgentContext" in annotation_str:
194
+ return True
195
+
196
+ except Exception: # noqa: BLE001
197
+ pass
198
+
199
+ return False
200
+
201
+ def generate_return_model(self) -> str | None:
202
+ """Generate Pydantic model code for the tool's return type."""
203
+ try:
204
+ schema = create_schema(self.callable)
205
+ if schema.returns.get("type") not in {"object", "array"}:
206
+ return None
207
+
208
+ class_name = f"{self.name.title()}Response"
209
+ model_code = schema.to_pydantic_model_code(class_name=class_name)
210
+ return model_code.strip() or None
211
+
212
+ except Exception: # noqa: BLE001
213
+ return None
214
+
215
+ # Route generation methods
216
+ def generate_route_handler(self) -> Callable:
217
+ """Generate FastAPI route handler for this tool.
218
+
219
+ Returns:
220
+ Async route handler function
221
+ """
222
+ # Extract parameter schema
223
+ schema = self.schema["function"]
224
+ parameters_schema = schema.get("parameters", {})
225
+
226
+ # Create parameter model
227
+ param_cls = create_param_model(dict(parameters_schema))
228
+
229
+ # Create route handler
230
+ return create_route_handler(self.callable, param_cls)
231
+
232
+ def add_route_to_app(self, app: FastAPI, path_prefix: str = "/tools") -> None:
233
+ """Add this tool's route to FastAPI app.
234
+
235
+ Args:
236
+ app: FastAPI application instance
237
+ path_prefix: Path prefix for the route
238
+ """
239
+ # Extract parameter schema
240
+ schema = self.schema["function"]
241
+ parameters_schema = schema.get("parameters", {})
242
+
243
+ # Create parameter model
244
+ param_cls = create_param_model(dict(parameters_schema))
245
+
246
+ # Create the route handler
247
+ route_handler = self.generate_route_handler()
248
+
249
+ # Set up the route with proper parameter annotations for FastAPI
250
+ if param_cls:
251
+ # Get field information from the generated model
252
+ model_fields = param_cls.model_fields
253
+ func_code = generate_func_code(model_fields)
254
+ # Execute the dynamic function creation
255
+ namespace = {"route_handler": route_handler, "Any": Any}
256
+ exec(func_code, namespace)
257
+ dynamic_handler: Callable = namespace["dynamic_handler"] # type: ignore
258
+ else:
259
+
260
+ async def dynamic_handler() -> dict[str, Any]:
261
+ return await route_handler()
262
+
263
+ # Add route to FastAPI app
264
+ app.get(f"{path_prefix}/{self.name}")(dynamic_handler)
265
+
266
+
267
+ if __name__ == "__main__":
268
+ import webbrowser
269
+
270
+ generator = ToolCodeGenerator.from_callable(webbrowser.open)
271
+ sig = generator.get_function_signature()
272
+ print(sig)
@@ -0,0 +1,151 @@
1
+ """Orchestrates code generation for multiple tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from schemez.code_generation.namespace_callable import NamespaceCallable
10
+ from schemez.code_generation.tool_code_generator import ToolCodeGenerator
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable, Sequence
15
+
16
+ from fastapi import FastAPI
17
+
18
+
19
+ USAGE = """\
20
+ Usage notes:
21
+ - Write your code inside an 'async def main():' function
22
+ - All tool functions are async, use 'await'
23
+ - Use 'return' statements to return values from main()
24
+ - Generated model classes are available for type checking
25
+ - Use 'await report_progress(current, total, message)' for long-running operations
26
+ - DO NOT call asyncio.run() or try to run the main function yourself
27
+ - DO NOT import asyncio or other modules - tools are already available
28
+ - Example:
29
+ async def main():
30
+ for i in range(5):
31
+ await report_progress(i, 5, f'Step {i+1} for {name}')
32
+ should_continue = await ask_user('Continue?', 'bool')
33
+ if not should_continue:
34
+ break
35
+ return f'Completed for {name}'
36
+
37
+ """
38
+
39
+
40
+ @dataclass
41
+ class ToolsetCodeGenerator:
42
+ """Generates code artifacts for multiple tools."""
43
+
44
+ generators: Sequence[ToolCodeGenerator]
45
+ """ToolCodeGenerator instances for each tool."""
46
+
47
+ include_signatures: bool = True
48
+ """Include function signatures in documentation."""
49
+
50
+ include_docstrings: bool = True
51
+ """Include function docstrings in documentation."""
52
+
53
+ @classmethod
54
+ def from_callables(
55
+ cls,
56
+ callables: Sequence[Callable],
57
+ include_signatures: bool = True,
58
+ include_docstrings: bool = True,
59
+ ) -> ToolsetCodeGenerator:
60
+ """Create a ToolsetCodeGenerator from a sequence of Tools.
61
+
62
+ Args:
63
+ callables: Callables to generate code for
64
+ include_signatures: Include function signatures in documentation
65
+ include_docstrings: Include function docstrings in documentation
66
+
67
+ Returns:
68
+ ToolsetCodeGenerator instance
69
+ """
70
+ generators = [ToolCodeGenerator.from_callable(i) for i in callables]
71
+ return cls(generators, include_signatures, include_docstrings)
72
+
73
+ def generate_tool_description(self) -> str:
74
+ """Generate comprehensive tool description with available functions."""
75
+ if not self.generators:
76
+ return "Execute Python code (no tools available)"
77
+
78
+ return_models = self.generate_return_models()
79
+ parts = [
80
+ "Execute Python code with the following tools available as async functions:",
81
+ "",
82
+ ]
83
+
84
+ if return_models:
85
+ parts.extend([
86
+ "# Generated return type models",
87
+ return_models,
88
+ "",
89
+ "# Available functions:",
90
+ "",
91
+ ])
92
+
93
+ for generator in self.generators:
94
+ if self.include_signatures:
95
+ signature = generator.get_function_signature()
96
+ parts.append(f"async def {signature}:")
97
+ else:
98
+ parts.append(f"async def {generator.name}(...):")
99
+
100
+ if self.include_docstrings and generator.callable.__doc__:
101
+ indented_desc = " " + generator.callable.__doc__.replace(
102
+ "\n", "\n "
103
+ )
104
+ parts.append(f' """{indented_desc}"""')
105
+ parts.append("")
106
+
107
+ parts.append(USAGE)
108
+
109
+ return "\n".join(parts)
110
+
111
+ def generate_execution_namespace(self) -> dict[str, Any]:
112
+ """Build Python namespace with tool functions and generated models."""
113
+ namespace: dict[str, Any] = {"__builtins__": __builtins__, "_result": None}
114
+
115
+ # Add tool functions
116
+ for generator in self.generators:
117
+ namespace[generator.name] = NamespaceCallable.from_generator(generator)
118
+
119
+ # Add generated model classes to namespace
120
+ if models_code := self.generate_return_models():
121
+ with contextlib.suppress(Exception):
122
+ exec(models_code, namespace)
123
+
124
+ return namespace
125
+
126
+ def generate_return_models(self) -> str:
127
+ """Generate Pydantic models for tool return types."""
128
+ model_parts = [
129
+ code for g in self.generators if (code := g.generate_return_model())
130
+ ]
131
+ return "\n\n".join(model_parts) if model_parts else ""
132
+
133
+ def add_all_routes(self, app: FastAPI, path_prefix: str = "/tools") -> None:
134
+ """Add FastAPI routes for all tools.
135
+
136
+ Args:
137
+ app: FastAPI application instance
138
+ path_prefix: Path prefix for routes
139
+ """
140
+ for generator in self.generators:
141
+ generator.add_route_to_app(app, path_prefix)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ import webbrowser
146
+
147
+ generator = ToolsetCodeGenerator.from_callables([webbrowser.open])
148
+ models = generator.generate_return_models()
149
+ print(models)
150
+ namespace = generator.generate_execution_namespace()
151
+ print(namespace)
schemez/schema.py CHANGED
@@ -2,17 +2,16 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Literal, Self
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Self
6
7
 
7
- import anyenv
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 from_vision_llm_sync(
73
- cls,
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
- file_content: The document content to create a schema from
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 document
78
+ A new schema model class based on the JSON schema
93
79
  """
94
- from llmling_agent import Agent, ImageBase64Content, PDFBase64Content
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
- if source_type == "pdf":
97
- content: BaseContent = PDFBase64Content.from_bytes(file_content)
98
- else:
99
- content = ImageBase64Content.from_bytes(file_content)
100
- agent = Agent[None]( # type:ignore[var-annotated]
101
- model=model,
102
- system_prompt=system_prompt.format(name=cls.__name__),
103
- provider=provider,
104
- ).to_structured(cls)
105
- chat_message = anyenv.run_sync(agent.run(user_prompt, content))
106
- return chat_message.content
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
- agent = Agent[None]( # type:ignore[var-annotated]
138
- model=model,
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
- agent = Agent[None]( # type:ignore[var-annotated]
200
- model=model,
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__)
@@ -33,7 +33,7 @@ class HttpToolExecutor:
33
33
 
34
34
  def __init__(
35
35
  self,
36
- schemas: Sequence[dict[str, Any] | UPath],
36
+ schemas: Sequence[dict[str, Any] | JoinablePathLike],
37
37
  handler: ToolHandler,
38
38
  base_url: str = "http://localhost:8000",
39
39
  ):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemez
3
- Version: 1.2.4
3
+ Version: 1.4.0
4
4
  Summary: Pydantic shim for config stuff
5
5
  Keywords:
6
6
  Author: Philipp Temminghoff
@@ -1,6 +1,11 @@
1
- schemez/__init__.py,sha256=KkwJF8pfbzw_4cBxwXuUyXIExqdxS8iumvyAL-JUED4,1669
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=yM29h4nccZEV21FMSZaKJI-IBdDezxpHld3n4WFWfkM,9627
8
+ schemez/code_generation/toolset_code_generator.py,sha256=Uw4UEjLzzHsm6YzsPFaEPxYHpFgIv_DKaFVP0XePjjI,5083
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
@@ -10,16 +15,16 @@ 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=Qt4INzB22jj-Uu61T-QI-USpNKMEeATdp98zE0Q8pL8,9592
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=-uSPfkE5JrpUQWbmD0QdEpAXVBsWiRmCWCcFvDWqtus,10486
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.2.4.dist-info/licenses/LICENSE,sha256=AteGCH9r177TxxrOFEiOARrastASsf7yW6MQxlAHdwA,1078
23
- schemez-1.2.4.dist-info/WHEEL,sha256=ELhySV62sOro8I5wRaLaF3TWxhBpkcDkdZUdAYLy_Hk,78
24
- schemez-1.2.4.dist-info/METADATA,sha256=ycelBanzLf1RK3HpheIamKERJ8lcEMdxmxmecfKotzk,11616
25
- schemez-1.2.4.dist-info/RECORD,,
27
+ schemez-1.4.0.dist-info/licenses/LICENSE,sha256=AteGCH9r177TxxrOFEiOARrastASsf7yW6MQxlAHdwA,1078
28
+ schemez-1.4.0.dist-info/WHEEL,sha256=DpNsHFUm_gffZe1FgzmqwuqiuPC6Y-uBCzibcJcdupM,78
29
+ schemez-1.4.0.dist-info/METADATA,sha256=PyncybCr23T22f_d0yl76k-YwHfXv2XHHGAGeuEgdw4,11616
30
+ schemez-1.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.3
2
+ Generator: uv 0.9.8
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any