schemez 1.1.1__py3-none-any.whl → 1.2.2__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.
schemez/helpers.py CHANGED
@@ -214,16 +214,16 @@ async def _detect_command(command: str, *, test_flag: str = "--version") -> list
214
214
 
215
215
 
216
216
  async def model_to_python_code(
217
- model: type[BaseModel],
217
+ model: type[BaseModel] | dict[str, Any],
218
218
  *,
219
219
  class_name: str | None = None,
220
220
  target_python_version: PythonVersion | None = None,
221
221
  model_type: str = "pydantic.BaseModel",
222
222
  ) -> str:
223
- """Convert a BaseModel to Python code asynchronously.
223
+ """Convert a BaseModel or schema dict to Python code asynchronously.
224
224
 
225
225
  Args:
226
- model: The BaseModel class to convert
226
+ model: The BaseModel class or schema dictionary to convert
227
227
  class_name: Optional custom class name for the generated code
228
228
  target_python_version: Target Python version for code generation.
229
229
  Defaults to current system Python version.
@@ -238,8 +238,12 @@ async def model_to_python_code(
238
238
  """
239
239
  working_prefix = await _detect_command("datamodel-codegen")
240
240
 
241
- schema = model.model_json_schema()
242
- name = class_name or model.__name__
241
+ if isinstance(model, dict):
242
+ schema = model
243
+ name = class_name or "GeneratedModel"
244
+ else:
245
+ schema = model.model_json_schema()
246
+ name = class_name or model.__name__
243
247
  py = target_python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
244
248
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
245
249
  # Use pydantic_core.to_json for proper schema serialization
schemez/log.py ADDED
@@ -0,0 +1,17 @@
1
+ """Logging configuration for schemez."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+
8
+ def get_logger(name: str) -> logging.Logger:
9
+ """Get a logger for the given name.
10
+
11
+ Args:
12
+ name: The name of the logger, will be prefixed with 'llmling_agent.'
13
+
14
+ Returns:
15
+ A logger instance
16
+ """
17
+ return logging.getLogger(f"schemez.{name}")
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable # noqa: TC003
4
+ import dataclasses
5
+ import importlib
6
+ import inspect
7
+ import types
8
+ from typing import Any, Literal, get_type_hints
9
+
10
+ import pydantic
11
+
12
+ from schemez.functionschema import (
13
+ FunctionSchema,
14
+ _resolve_type_annotation,
15
+ create_schema,
16
+ )
17
+ from schemez.typedefs import ToolParameters
18
+
19
+
20
+ def create_schemas_from_callables(
21
+ callables: dict[str, Callable[..., Any]],
22
+ prefix: str | Literal[False] | None = None,
23
+ exclude_private: bool = True,
24
+ ) -> dict[str, FunctionSchema]:
25
+ """Generate OpenAI function schemas from a dictionary of callables.
26
+
27
+ Args:
28
+ callables: Dictionary mapping names to callable objects
29
+ prefix: Schema name prefix to prepend to function names.
30
+ If None, no prefix. If False, use raw name.
31
+ If string, uses that prefix.
32
+ exclude_private: Whether to exclude callables starting with underscore
33
+
34
+ Returns:
35
+ Dictionary mapping qualified names to FunctionSchema objects
36
+
37
+ Example:
38
+ >>> def foo(x: int) -> str: ...
39
+ >>> def bar(y: float) -> int: ...
40
+ >>> callables = {'foo': foo, 'bar': bar}
41
+ >>> schemas = create_schemas_from_callables(callables, prefix='math')
42
+ >>> print(schemas['math.foo'])
43
+ """
44
+ schemas = {}
45
+
46
+ for name, callable_obj in callables.items():
47
+ # Skip private members if requested
48
+ if exclude_private and name.startswith("_"):
49
+ continue
50
+
51
+ # Generate schema key based on prefix setting
52
+ key = name if prefix is False else f"{prefix}.{name}" if prefix else name
53
+ schemas[key] = create_schema(callable_obj)
54
+
55
+ return schemas
56
+
57
+
58
+ def create_schemas_from_class(
59
+ cls: type,
60
+ prefix: str | Literal[False] | None = None,
61
+ ) -> dict[str, FunctionSchema]:
62
+ """Generate OpenAI function schemas for all public methods in a class.
63
+
64
+ Args:
65
+ cls: The class to generate schemas from
66
+ prefix: Schema name prefix. If None, uses class name.
67
+ If False, no prefix. If string, uses that prefix.
68
+
69
+ Returns:
70
+ Dictionary mapping qualified method names to FunctionSchema objects
71
+
72
+ Example:
73
+ >>> class MyClass:
74
+ ... def my_method(self, x: int) -> str:
75
+ ... return str(x)
76
+ >>> schemas = create_schemas_from_class(MyClass)
77
+ >>> print(schemas['MyClass.my_method'])
78
+ """
79
+ callables: dict[str, Callable[..., Any]] = {}
80
+
81
+ # Get all attributes of the class
82
+ for name, attr in inspect.getmembers(cls):
83
+ # Handle different method types
84
+ if inspect.isfunction(attr) or inspect.ismethod(attr):
85
+ callables[name] = attr
86
+ elif isinstance(attr, classmethod | staticmethod):
87
+ callables[name] = attr.__get__(None, cls)
88
+
89
+ # Use default prefix of class name if not specified
90
+ effective_prefix = cls.__name__ if prefix is None else prefix
91
+ return create_schemas_from_callables(callables, prefix=effective_prefix)
92
+
93
+
94
+ def create_constructor_schema(cls: type) -> FunctionSchema:
95
+ """Create OpenAI function schema from class constructor.
96
+
97
+ Args:
98
+ cls: Class to create schema for
99
+
100
+ Returns:
101
+ OpenAI function schema for class constructor
102
+ """
103
+ if isinstance(cls, type) and issubclass(cls, pydantic.BaseModel):
104
+ properties = {}
105
+ required = []
106
+ for name, field in cls.model_fields.items():
107
+ param_type = field.annotation
108
+ properties[name] = _resolve_type_annotation(
109
+ param_type,
110
+ description=field.description,
111
+ default=field.default,
112
+ )
113
+ if field.is_required():
114
+ required.append(name)
115
+
116
+ # Handle dataclasses
117
+ elif dataclasses.is_dataclass(cls):
118
+ properties = {}
119
+ required = []
120
+ dc_fields = dataclasses.fields(cls)
121
+ hints = get_type_hints(cls)
122
+
123
+ for dc_field in dc_fields:
124
+ param_type = hints[dc_field.name]
125
+ properties[dc_field.name] = _resolve_type_annotation(
126
+ param_type, default=dc_field.default
127
+ )
128
+ if (
129
+ dc_field.default is dataclasses.MISSING
130
+ and dc_field.default_factory is dataclasses.MISSING
131
+ ):
132
+ required.append(dc_field.name)
133
+
134
+ # Handle regular classes
135
+ else:
136
+ sig = inspect.signature(cls.__init__)
137
+ hints = get_type_hints(cls.__init__)
138
+ properties = {}
139
+ required = []
140
+
141
+ for name, param in sig.parameters.items():
142
+ if name == "self":
143
+ continue
144
+
145
+ param_type = hints.get(name, Any)
146
+ properties[name] = _resolve_type_annotation(
147
+ param_type,
148
+ default=param.default,
149
+ )
150
+
151
+ if param.default is param.empty:
152
+ required.append(name)
153
+
154
+ name = f"create_{cls.__name__}"
155
+ description = inspect.getdoc(cls) or f"Create {cls.__name__} instance"
156
+
157
+ # Create parameters with required list included
158
+ params = ToolParameters({
159
+ "type": "object",
160
+ "properties": properties,
161
+ "required": required,
162
+ })
163
+
164
+ return FunctionSchema(
165
+ name=name, description=description, parameters=params, required=required
166
+ )
167
+
168
+
169
+ def create_schemas_from_module(
170
+ module: types.ModuleType | str,
171
+ include_functions: list[str] | None = None,
172
+ prefix: str | Literal[False] | None = None,
173
+ ) -> dict[str, FunctionSchema]:
174
+ """Generate OpenAI function schemas from a Python module's functions.
175
+
176
+ Args:
177
+ module: Either a ModuleType object or string name of module to analyze
178
+ include_functions: Optional list of function names to specifically include
179
+ prefix: Schema name prefix. If None, uses module name.
180
+ If False, no prefix. If string, uses that prefix.
181
+
182
+ Returns:
183
+ Dictionary mapping function names to FunctionSchema objects
184
+
185
+ Raises:
186
+ ImportError: If module string name cannot be imported
187
+
188
+ Example:
189
+ >>> import math
190
+ >>> schemas = create_schemas_from_module(math, ['sin', 'cos'])
191
+ >>> print(schemas['math.sin'])
192
+ """
193
+ # Resolve module if string name provided
194
+ mod = (
195
+ module
196
+ if isinstance(module, types.ModuleType)
197
+ else importlib.import_module(module)
198
+ )
199
+
200
+ # Get all functions from module
201
+ callables: dict[str, Callable[..., Any]] = {
202
+ name: func
203
+ for name, func in inspect.getmembers(mod, predicate=inspect.isfunction)
204
+ if include_functions is None
205
+ or (name in include_functions and func.__module__.startswith(mod.__name__))
206
+ }
207
+
208
+ # Use default prefix of module name if not specified
209
+ effective_prefix = mod.__name__ if prefix is None else prefix
210
+ return create_schemas_from_callables(callables, prefix=effective_prefix)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ schemas = create_schemas_from_module(__name__)
215
+ print(schemas)
@@ -0,0 +1,8 @@
1
+ """Tool executor module for HTTP tool generation and execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from schemez.tool_executor.executor import HttpToolExecutor
6
+ from schemez.tool_executor.types import ToolHandler
7
+
8
+ __all__ = ["HttpToolExecutor", "ToolHandler"]
@@ -0,0 +1,322 @@
1
+ """HTTP tool executor for managing tool generation and execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from pathlib import Path
8
+ import time
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from pydantic import BaseModel
12
+ from pydantic_core import from_json
13
+ from upath import UPath
14
+
15
+ from schemez.functionschema import FunctionSchema
16
+ from schemez.tool_executor.helpers import clean_generated_code, generate_input_model
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable, Sequence
21
+
22
+ from fastapi import FastAPI
23
+
24
+ from schemez.tool_executor.types import ToolHandler
25
+
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class HttpToolExecutor:
31
+ """Manages HTTP tool generation and execution."""
32
+
33
+ def __init__(
34
+ self,
35
+ schemas: Sequence[dict[str, Any] | Path],
36
+ handler: ToolHandler,
37
+ base_url: str = "http://localhost:8000",
38
+ ):
39
+ """Initialize the tool executor.
40
+
41
+ Args:
42
+ schemas: List of tool schema dictionaries or file paths
43
+ handler: User-provided tool handler function
44
+ base_url: Base URL for the tool server
45
+ """
46
+ self.schemas = schemas
47
+ self.handler = handler
48
+ self.base_url = base_url
49
+
50
+ # Cached artifacts
51
+ self._tool_mappings: dict[str, str] | None = None
52
+ self._tools_code: str | None = None
53
+ self._server_app: FastAPI | None = None
54
+ self._tool_functions: dict[str, Callable] | None = None
55
+
56
+ async def _load_schemas(self) -> list[dict]:
57
+ """Load and normalize schemas from various sources."""
58
+ loaded_schemas = []
59
+
60
+ for schema in self.schemas:
61
+ match schema:
62
+ case dict():
63
+ loaded_schemas.append(schema)
64
+ case str() | Path():
65
+ text = UPath(schema).read_text("utf-8")
66
+ loaded_schemas.append(from_json(text))
67
+ case _:
68
+ msg = f"Invalid schema type: {type(schema)}"
69
+ raise TypeError(msg)
70
+
71
+ return loaded_schemas
72
+
73
+ async def _get_tool_mappings(self) -> dict[str, str]:
74
+ """Get tool name to input class mappings."""
75
+ if self._tool_mappings is None:
76
+ self._tool_mappings = {}
77
+ schemas = await self._load_schemas()
78
+
79
+ for schema_dict in schemas:
80
+ function_schema = FunctionSchema.from_dict(schema_dict)
81
+ name = "".join(word.title() for word in function_schema.name.split("_"))
82
+ input_class_name = f"{name}Input"
83
+ self._tool_mappings[function_schema.name] = input_class_name
84
+
85
+ return self._tool_mappings
86
+
87
+ async def _generate_http_wrapper(
88
+ self, schema_dict: dict, input_class_name: str
89
+ ) -> str:
90
+ """Generate HTTP wrapper function."""
91
+ name = schema_dict["name"]
92
+ description = schema_dict.get("description", "")
93
+
94
+ return f'''
95
+ async def {name}(input: {input_class_name}) -> str:
96
+ """{description}
97
+
98
+ Args:
99
+ input: Function parameters
100
+
101
+ Returns:
102
+ String response from the tool server
103
+ """
104
+ import httpx
105
+ async with httpx.AsyncClient() as client:
106
+ response = await client.post(
107
+ "{self.base_url}/tools/{name}",
108
+ json=input.model_dump(),
109
+ timeout=30.0
110
+ )
111
+ response.raise_for_status()
112
+ return response.text
113
+ '''
114
+
115
+ async def generate_tools_code(self) -> str:
116
+ """Generate HTTP wrapper tools as Python code."""
117
+ if self._tools_code is not None:
118
+ return self._tools_code
119
+
120
+ start_time = time.time()
121
+ logger.info("Starting tools code generation")
122
+
123
+ schemas = await self._load_schemas()
124
+ code_parts: list[str] = []
125
+ await self._get_tool_mappings()
126
+
127
+ # Module header
128
+ header = '''"""Generated HTTP wrapper tools."""
129
+
130
+ from __future__ import annotations
131
+
132
+ from pydantic import BaseModel, Field
133
+ from typing import Literal, List, Any
134
+ from datetime import datetime
135
+
136
+ '''
137
+ code_parts.append(header)
138
+
139
+ # Generate models and wrappers for each tool
140
+ all_exports = []
141
+ for schema_dict in schemas:
142
+ function_schema = FunctionSchema.from_dict(schema_dict)
143
+
144
+ schema_data = {
145
+ "name": function_schema.name,
146
+ "description": function_schema.description,
147
+ "parameters": function_schema.parameters,
148
+ }
149
+
150
+ # Generate input model (strip future imports from generated code)
151
+ input_code, input_class_name = await generate_input_model(schema_data)
152
+ # Remove future imports and datamodel-codegen header from individual models
153
+ cleaned_input_code = clean_generated_code(input_code)
154
+ code_parts.append(cleaned_input_code)
155
+ wrapper_code = await self._generate_http_wrapper(
156
+ schema_data, input_class_name
157
+ )
158
+ code_parts.append(wrapper_code)
159
+
160
+ all_exports.extend([input_class_name, function_schema.name])
161
+
162
+ # Add exports
163
+ code_parts.append(f"\n__all__ = {all_exports}\n")
164
+
165
+ self._tools_code = "\n".join(code_parts)
166
+ elapsed = time.time() - start_time
167
+ logger.info(f"Tools code generation completed in {elapsed:.2f}s") # noqa: G004
168
+ return self._tools_code
169
+
170
+ async def generate_server_app(self) -> FastAPI:
171
+ """Create configured FastAPI server."""
172
+ from fastapi import FastAPI, HTTPException
173
+
174
+ if self._server_app is not None:
175
+ return self._server_app
176
+
177
+ tool_mappings = await self._get_tool_mappings()
178
+ app = FastAPI(title="Tool Server", version="1.0.0")
179
+
180
+ @app.post("/tools/{tool_name}")
181
+ async def handle_tool_call(tool_name: str, input_data: dict) -> str:
182
+ """Generic endpoint that routes all tool calls to user handler."""
183
+ # Validate tool exists
184
+ if tool_name not in tool_mappings:
185
+ tools = list(tool_mappings.keys())
186
+ detail = f"Tool '{tool_name}' not found. Available: {tools}"
187
+ raise HTTPException(status_code=404, detail=detail)
188
+
189
+ # Create a simple BaseModel for validation
190
+ class DynamicInput(BaseModel):
191
+ pass
192
+
193
+ # Add fields dynamically (basic validation only)
194
+ dynamic_input = DynamicInput(**input_data)
195
+
196
+ # Call user's handler
197
+ try:
198
+ return await self.handler(tool_name, dynamic_input)
199
+ except Exception as e:
200
+ msg = f"Tool execution failed: {e}"
201
+ raise HTTPException(status_code=500, detail=msg) from e
202
+
203
+ self._server_app = app
204
+ return app
205
+
206
+ async def get_tool_functions(self) -> dict[str, Callable]:
207
+ """Get ready-to-use tool functions."""
208
+ if self._tool_functions is not None:
209
+ return self._tool_functions
210
+
211
+ start_time = time.time()
212
+ logger.info("Starting tool functions generation")
213
+
214
+ # Generate and execute the tools code
215
+ tools_code = await self.generate_tools_code()
216
+ logger.debug("Generated %s characters of code", len(tools_code))
217
+
218
+ # Create namespace and execute
219
+ namespace = {
220
+ "BaseModel": BaseModel,
221
+ "Field": BaseModel.model_fields_set,
222
+ "Literal": Any,
223
+ "List": list,
224
+ "datetime": __import__("datetime").datetime,
225
+ }
226
+
227
+ logger.debug("Executing generated tools code...")
228
+ exec_start = time.time()
229
+ exec(tools_code, namespace)
230
+ exec_elapsed = time.time() - exec_start
231
+ logger.debug("Code execution completed in %.2fs", exec_elapsed)
232
+
233
+ # Extract tool functions
234
+ tool_mappings = await self._get_tool_mappings()
235
+ self._tool_functions = {
236
+ tool_name: namespace[tool_name]
237
+ for tool_name in tool_mappings
238
+ if tool_name in namespace
239
+ }
240
+
241
+ elapsed = time.time() - start_time
242
+ logger.info(f"Tool functions generation completed in {elapsed:.2f}s") # noqa: G004
243
+ return self._tool_functions
244
+
245
+ async def start_server(
246
+ self, host: str = "0.0.0.0", port: int = 8000, background: bool = False
247
+ ) -> None | asyncio.Task:
248
+ """Start the FastAPI server.
249
+
250
+ Args:
251
+ host: Host to bind to
252
+ port: Port to bind to
253
+ background: If True, run server in background task
254
+ """
255
+ import uvicorn
256
+
257
+ app = await self.generate_server_app()
258
+
259
+ if background:
260
+ config = uvicorn.Config(app, host=host, port=port)
261
+ server = uvicorn.Server(config)
262
+ return asyncio.create_task(server.serve())
263
+
264
+ uvicorn.run(app, host=host, port=port)
265
+ return None
266
+
267
+ async def save_to_files(self, output_dir: Path) -> dict[str, Path]:
268
+ """Save generated code to files.
269
+
270
+ Args:
271
+ output_dir: Directory to save files to
272
+
273
+ Returns:
274
+ Dictionary mapping file types to paths
275
+ """
276
+ output_dir = Path(output_dir)
277
+ output_dir.mkdir(parents=True, exist_ok=True)
278
+
279
+ saved_files = {}
280
+
281
+ # Save tools module
282
+ tools_code = await self.generate_tools_code()
283
+ tools_file = output_dir / "generated_tools.py"
284
+ tools_file.write_text(tools_code)
285
+ saved_files["tools"] = tools_file
286
+
287
+ # Save server code (as template/example)
288
+ server_template = f'''"""FastAPI server using HttpToolExecutor."""
289
+
290
+ import asyncio
291
+ from pathlib import Path
292
+
293
+ from schemez.tool_executor import HttpToolExecutor, ToolHandler
294
+ from pydantic import BaseModel
295
+
296
+
297
+ async def my_tool_handler(method_name: str, input_props: BaseModel) -> str:
298
+ """Implement your tool logic here."""
299
+ match method_name:
300
+ case _:
301
+ return f"Mock result for {{method_name}}: {{input_props}}"
302
+
303
+
304
+ async def main():
305
+ """Start the server with your handler."""
306
+ executor = HttpToolExecutor(
307
+ schemas=[], # Add your schema files/dicts here
308
+ handler=my_tool_handler,
309
+ base_url="{self.base_url}"
310
+ )
311
+
312
+ await executor.start_server()
313
+
314
+
315
+ if __name__ == "__main__":
316
+ asyncio.run(main())
317
+ '''
318
+
319
+ server_file = output_dir / "server_example.py"
320
+ server_file.write_text(server_template)
321
+ saved_files["server_example"] = server_file
322
+ return saved_files
@@ -0,0 +1,46 @@
1
+ """Helper functions for the tool executor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+
8
+ from schemez.helpers import model_to_python_code
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ async def generate_input_model(schema_dict: dict) -> tuple[str, str]:
15
+ """Generate input model code from schema."""
16
+ start_time = time.time()
17
+ logger.debug("Generating input model for %s", schema_dict["name"])
18
+
19
+ words = [word.title() for word in schema_dict["name"].split("_")]
20
+ cls_name = f"{''.join(words)}Input"
21
+ code = await model_to_python_code(schema_dict["parameters"], class_name=cls_name)
22
+ elapsed = time.time() - start_time
23
+ logger.debug("Generated input model for %s in %.2fs", schema_dict["name"], elapsed)
24
+ return code, cls_name
25
+
26
+
27
+ def clean_generated_code(code: str) -> str:
28
+ """Clean generated code by removing future imports and headers."""
29
+ lines = code.split("\n")
30
+ cleaned_lines = []
31
+ skip_until_class = True
32
+
33
+ for line in lines:
34
+ # Skip lines until we find a class or other meaningful content
35
+ if skip_until_class:
36
+ if line.strip().startswith("class ") or (
37
+ line.strip()
38
+ and not line.startswith("#")
39
+ and not line.startswith("from __future__")
40
+ ):
41
+ skip_until_class = False
42
+ cleaned_lines.append(line)
43
+ continue
44
+ cleaned_lines.append(line)
45
+
46
+ return "\n".join(cleaned_lines)
@@ -0,0 +1,28 @@
1
+ """Type definitions for tool executor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class ToolHandler(Protocol):
13
+ """Protocol for user-provided tool handler."""
14
+
15
+ async def __call__(self, method_name: str, input_props: BaseModel) -> str:
16
+ """Process a tool call.
17
+
18
+ Args:
19
+ method_name: Name of the tool being called (e.g., "get_weather")
20
+ input_props: Validated input model instance
21
+
22
+ Returns:
23
+ String result from the tool execution
24
+
25
+ Raises:
26
+ Exception: If tool execution fails
27
+ """
28
+ ...