schemez 1.2.2__tar.gz → 1.4.1__tar.gz
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-1.2.2 → schemez-1.4.1}/PKG-INFO +1 -1
- {schemez-1.2.2 → schemez-1.4.1}/pyproject.toml +1 -1
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/__init__.py +4 -0
- schemez-1.4.1/src/schemez/code_generation/__init__.py +6 -0
- schemez-1.4.1/src/schemez/code_generation/namespace_callable.py +76 -0
- schemez-1.4.1/src/schemez/code_generation/route_helpers.py +126 -0
- schemez-1.4.1/src/schemez/code_generation/tool_code_generator.py +272 -0
- schemez-1.4.1/src/schemez/code_generation/toolset_code_generator.py +159 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/executable.py +11 -9
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/functionschema.py +22 -42
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/helpers.py +5 -40
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/schema.py +131 -82
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/tool_executor/executor.py +7 -6
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/tool_executor/helpers.py +2 -2
- {schemez-1.2.2 → schemez-1.4.1}/LICENSE +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/README.md +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/bind_kwargs.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/code.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/convert.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/create_type.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/docstrings.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/log.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/py.typed +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/pydantic_types.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/schema_generators.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/schemadef/__init__.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/schemadef/schemadef.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/tool_executor/__init__.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/tool_executor/types.py +0 -0
- {schemez-1.2.2 → schemez-1.4.1}/src/schemez/typedefs.py +0 -0
|
@@ -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
|
+
callable: Callable
|
|
42
|
+
"""Tool to generate code for."""
|
|
43
|
+
|
|
44
|
+
schema: OpenAIFunctionTool
|
|
45
|
+
"""Schema of the tool."""
|
|
46
|
+
|
|
47
|
+
name_override: str | None = None
|
|
48
|
+
"""Name override for the function to generate code for."""
|
|
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,159 @@
|
|
|
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
|
+
) -> ToolsetCodeGenerator:
|
|
61
|
+
"""Create a ToolsetCodeGenerator from a sequence of Tools.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
callables: Callables to generate code for
|
|
65
|
+
include_signatures: Include function signatures in documentation
|
|
66
|
+
include_docstrings: Include function docstrings in documentation
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
ToolsetCodeGenerator instance
|
|
70
|
+
"""
|
|
71
|
+
generators = [ToolCodeGenerator.from_callable(i) for i in callables]
|
|
72
|
+
return cls(generators, include_signatures, include_docstrings)
|
|
73
|
+
|
|
74
|
+
def generate_tool_description(self) -> str:
|
|
75
|
+
"""Generate comprehensive tool description with available functions."""
|
|
76
|
+
if not self.generators:
|
|
77
|
+
return "Execute Python code (no tools available)"
|
|
78
|
+
|
|
79
|
+
return_models = self.generate_return_models()
|
|
80
|
+
parts = [
|
|
81
|
+
"Execute Python code with the following tools available as async functions:",
|
|
82
|
+
"",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if return_models:
|
|
86
|
+
parts.extend([
|
|
87
|
+
"# Generated return type models",
|
|
88
|
+
return_models,
|
|
89
|
+
"",
|
|
90
|
+
"# Available functions:",
|
|
91
|
+
"",
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
for generator in self.generators:
|
|
95
|
+
if self.include_signatures:
|
|
96
|
+
signature = generator.get_function_signature()
|
|
97
|
+
parts.append(f"async def {signature}:")
|
|
98
|
+
else:
|
|
99
|
+
parts.append(f"async def {generator.name}(...):")
|
|
100
|
+
|
|
101
|
+
if self.include_docstrings and generator.callable.__doc__:
|
|
102
|
+
indented_desc = " " + generator.callable.__doc__.replace(
|
|
103
|
+
"\n", "\n "
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Add warning for async functions without proper return type hints
|
|
107
|
+
if inspect.iscoroutinefunction(generator.callable):
|
|
108
|
+
sig = inspect.signature(generator.callable)
|
|
109
|
+
if sig.return_annotation == inspect.Signature.empty:
|
|
110
|
+
indented_desc += "\n \n Note: This async function should explicitly return a value." # noqa: E501
|
|
111
|
+
|
|
112
|
+
parts.append(f' """{indented_desc}"""')
|
|
113
|
+
parts.append("")
|
|
114
|
+
|
|
115
|
+
parts.append(USAGE)
|
|
116
|
+
|
|
117
|
+
return "\n".join(parts)
|
|
118
|
+
|
|
119
|
+
def generate_execution_namespace(self) -> dict[str, Any]:
|
|
120
|
+
"""Build Python namespace with tool functions and generated models."""
|
|
121
|
+
namespace: dict[str, Any] = {"__builtins__": __builtins__, "_result": None}
|
|
122
|
+
|
|
123
|
+
# Add tool functions
|
|
124
|
+
for generator in self.generators:
|
|
125
|
+
namespace[generator.name] = NamespaceCallable.from_generator(generator)
|
|
126
|
+
|
|
127
|
+
# Add generated model classes to namespace
|
|
128
|
+
if models_code := self.generate_return_models():
|
|
129
|
+
with contextlib.suppress(Exception):
|
|
130
|
+
exec(models_code, namespace)
|
|
131
|
+
|
|
132
|
+
return namespace
|
|
133
|
+
|
|
134
|
+
def generate_return_models(self) -> str:
|
|
135
|
+
"""Generate Pydantic models for tool return types."""
|
|
136
|
+
model_parts = [
|
|
137
|
+
code for g in self.generators if (code := g.generate_return_model())
|
|
138
|
+
]
|
|
139
|
+
return "\n\n".join(model_parts) if model_parts else ""
|
|
140
|
+
|
|
141
|
+
def add_all_routes(self, app: FastAPI, path_prefix: str = "/tools") -> None:
|
|
142
|
+
"""Add FastAPI routes for all tools.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
app: FastAPI application instance
|
|
146
|
+
path_prefix: Path prefix for routes
|
|
147
|
+
"""
|
|
148
|
+
for generator in self.generators:
|
|
149
|
+
generator.add_route_to_app(app, path_prefix)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
import webbrowser
|
|
154
|
+
|
|
155
|
+
generator = ToolsetCodeGenerator.from_callables([webbrowser.open])
|
|
156
|
+
models = generator.generate_return_models()
|
|
157
|
+
print(models)
|
|
158
|
+
namespace = generator.generate_execution_namespace()
|
|
159
|
+
print(namespace)
|