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/__init__.py +21 -0
- schemez/bind_kwargs.py +193 -0
- schemez/convert.py +1 -1
- schemez/create_type.py +340 -0
- schemez/executable.py +211 -0
- schemez/functionschema.py +772 -0
- schemez/helpers.py +9 -5
- schemez/log.py +17 -0
- schemez/schema_generators.py +215 -0
- schemez/tool_executor/__init__.py +8 -0
- schemez/tool_executor/executor.py +322 -0
- schemez/tool_executor/helpers.py +46 -0
- schemez/tool_executor/types.py +28 -0
- schemez/typedefs.py +205 -0
- schemez-1.2.2.dist-info/METADATA +340 -0
- schemez-1.2.2.dist-info/RECORD +25 -0
- {schemez-1.1.1.dist-info → schemez-1.2.2.dist-info}/WHEEL +1 -1
- schemez-1.2.2.dist-info/licenses/LICENSE +22 -0
- schemez-1.1.1.dist-info/METADATA +0 -85
- schemez-1.1.1.dist-info/RECORD +0 -13
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
|
-
|
242
|
-
|
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
|
+
...
|