morphsdk 0.2.5__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.
- morphsdk/__init__.py +54 -0
- morphsdk/_agent/__init__.py +64 -0
- morphsdk/_agent/config.py +52 -0
- morphsdk/_agent/explore.py +276 -0
- morphsdk/_agent/github.py +57 -0
- morphsdk/_agent/helpers.py +133 -0
- morphsdk/_agent/parser.py +163 -0
- morphsdk/_agent/runner.py +524 -0
- morphsdk/_agent/tools.py +171 -0
- morphsdk/_agent/types.py +126 -0
- morphsdk/_base.py +309 -0
- morphsdk/_client.py +245 -0
- morphsdk/_config.py +37 -0
- morphsdk/_constants.py +53 -0
- morphsdk/_errors.py +111 -0
- morphsdk/_providers/__init__.py +36 -0
- morphsdk/_providers/_filter.py +92 -0
- morphsdk/_providers/base.py +94 -0
- morphsdk/_providers/code_storage_http.py +104 -0
- morphsdk/_providers/local.py +270 -0
- morphsdk/_providers/remote.py +161 -0
- morphsdk/_version.py +1 -0
- morphsdk/adapters/__init__.py +1 -0
- morphsdk/adapters/anthropic.py +360 -0
- morphsdk/adapters/langchain.py +120 -0
- morphsdk/adapters/openai.py +500 -0
- morphsdk/py.typed +0 -0
- morphsdk/resources/__init__.py +0 -0
- morphsdk/resources/browser.py +919 -0
- morphsdk/resources/compact.py +133 -0
- morphsdk/resources/edit.py +506 -0
- morphsdk/resources/explore.py +333 -0
- morphsdk/resources/git.py +861 -0
- morphsdk/resources/github.py +1214 -0
- morphsdk/resources/grep.py +583 -0
- morphsdk/resources/mobile.py +134 -0
- morphsdk/resources/reflex.py +414 -0
- morphsdk/resources/router.py +124 -0
- morphsdk/resources/search.py +110 -0
- morphsdk/tracing/__init__.py +70 -0
- morphsdk/tracing/_otel.py +101 -0
- morphsdk/tracing/core.py +249 -0
- morphsdk/tracing/interaction.py +284 -0
- morphsdk/tracing/otel.py +75 -0
- morphsdk/tracing/reflex.py +58 -0
- morphsdk/tracing/types.py +163 -0
- morphsdk/types/__init__.py +140 -0
- morphsdk/types/browser.py +118 -0
- morphsdk/types/compact.py +41 -0
- morphsdk/types/edit.py +31 -0
- morphsdk/types/explore.py +42 -0
- morphsdk/types/git.py +25 -0
- morphsdk/types/github.py +111 -0
- morphsdk/types/grep.py +41 -0
- morphsdk/types/mobile.py +25 -0
- morphsdk/types/reflex.py +137 -0
- morphsdk/types/router.py +21 -0
- morphsdk/types/search.py +33 -0
- morphsdk-0.2.5.dist-info/METADATA +226 -0
- morphsdk-0.2.5.dist-info/RECORD +61 -0
- morphsdk-0.2.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Anthropic framework adapter -- generates Anthropic Tool-compatible dicts.
|
|
2
|
+
|
|
3
|
+
Each tool definition follows the Anthropic tool schema::
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
"name": "...",
|
|
7
|
+
"description": "...",
|
|
8
|
+
"input_schema": { JSON Schema }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
The :class:`AnthropicTool` wrapper pairs the definition with an ``execute()``
|
|
12
|
+
method so callers can dispatch tool calls in a single object.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from collections.abc import ItemsView, Iterator, KeysView, ValuesView
|
|
19
|
+
from typing import Any, Callable
|
|
20
|
+
|
|
21
|
+
from .openai import (
|
|
22
|
+
BROWSER_TOOL_DESCRIPTION,
|
|
23
|
+
CODEBASE_SEARCH_DESCRIPTION,
|
|
24
|
+
EDIT_FILE_DESCRIPTION,
|
|
25
|
+
GITHUB_READ_FILE_DESCRIPTION,
|
|
26
|
+
GITHUB_SEARCH_DESCRIPTION,
|
|
27
|
+
MOBILE_TOOL_DESCRIPTION,
|
|
28
|
+
WARP_GREP_DESCRIPTION,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Tool definition factories
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def edit_file_tool_def(*, description: str | None = None) -> dict[str, Any]:
|
|
36
|
+
"""Generate an Anthropic Tool dict for ``edit_file``."""
|
|
37
|
+
return {
|
|
38
|
+
"name": "edit_file",
|
|
39
|
+
"description": description or EDIT_FILE_DESCRIPTION,
|
|
40
|
+
"input_schema": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"target_filepath": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "The path of the target file to modify",
|
|
46
|
+
},
|
|
47
|
+
"instruction": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": (
|
|
50
|
+
"A single sentence written in the first person describing "
|
|
51
|
+
"what the agent is changing. Used to help disambiguate "
|
|
52
|
+
"uncertainty in the edit."
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
"code_edit": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": (
|
|
58
|
+
"Specify ONLY the precise lines of code that you wish to "
|
|
59
|
+
"edit. Use `// ... existing code ...` for unchanged sections."
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"required": ["target_filepath", "instruction", "code_edit"],
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def codebase_search_tool_def(*, description: str | None = None) -> dict[str, Any]:
|
|
69
|
+
"""Generate an Anthropic Tool dict for ``codebase_search``."""
|
|
70
|
+
return {
|
|
71
|
+
"name": "codebase_search",
|
|
72
|
+
"description": description or CODEBASE_SEARCH_DESCRIPTION,
|
|
73
|
+
"input_schema": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"explanation": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": (
|
|
79
|
+
"One sentence explanation as to why this tool is being used, "
|
|
80
|
+
"and how it contributes to the goal."
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
"query": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": (
|
|
86
|
+
"A complete question about what you want to understand. "
|
|
87
|
+
"Ask as if talking to a colleague: \"How does X work?\", "
|
|
88
|
+
"\"What happens when Y?\", \"Where is Z handled?\""
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
"target_directories": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"items": {"type": "string"},
|
|
94
|
+
"description": (
|
|
95
|
+
"Prefix directory paths to limit search scope "
|
|
96
|
+
"(single directory only, no glob patterns). "
|
|
97
|
+
"Use [] to search entire repo."
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
"limit": {
|
|
101
|
+
"type": "number",
|
|
102
|
+
"description": "Maximum results to return (default: 10)",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"required": ["query", "target_directories", "explanation"],
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def grep_tool_def(
|
|
111
|
+
*,
|
|
112
|
+
name: str = "codebase_search",
|
|
113
|
+
description: str | None = None,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Generate an Anthropic Tool dict for the WarpGrep agent."""
|
|
116
|
+
return {
|
|
117
|
+
"name": name,
|
|
118
|
+
"description": description or WARP_GREP_DESCRIPTION,
|
|
119
|
+
"input_schema": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"search_term": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": (
|
|
125
|
+
"Search problem statement that this subagent is "
|
|
126
|
+
"supposed to research for"
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"required": ["search_term"],
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def github_search_tool_def(*, description: str | None = None) -> dict[str, Any]:
|
|
136
|
+
"""Generate an Anthropic Tool dict for ``github_codebase_search``."""
|
|
137
|
+
return {
|
|
138
|
+
"name": "github_codebase_search",
|
|
139
|
+
"description": description or GITHUB_SEARCH_DESCRIPTION,
|
|
140
|
+
"input_schema": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"properties": {
|
|
143
|
+
"search_term": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": (
|
|
146
|
+
"A targeted natural-language query describing what to find "
|
|
147
|
+
"(e.g. \"How does auth work for SSO users?\"). "
|
|
148
|
+
"Not a keyword or symbol name."
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
"github_url": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"description": (
|
|
154
|
+
"GitHub repository URL to search "
|
|
155
|
+
"(e.g. \"https://github.com/vercel/next.js\"). "
|
|
156
|
+
"You must provide either github_url or owner_repo."
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
"owner_repo": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"description": (
|
|
162
|
+
"Repository owner/repo shorthand (e.g. \"vercel/next.js\"). "
|
|
163
|
+
"You must provide either github_url or owner_repo."
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
"branch": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "Branch to search (defaults to the repository default branch)",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
"required": ["search_term"],
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def github_read_file_tool_def(*, description: str | None = None) -> dict[str, Any]:
|
|
177
|
+
"""Generate an Anthropic Tool dict for ``readfile_github_search``."""
|
|
178
|
+
return {
|
|
179
|
+
"name": "readfile_github_search",
|
|
180
|
+
"description": description or GITHUB_READ_FILE_DESCRIPTION,
|
|
181
|
+
"input_schema": {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"properties": {
|
|
184
|
+
"github": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "owner/repo shorthand (e.g., \"vercel/next.js\")",
|
|
187
|
+
},
|
|
188
|
+
"path": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": (
|
|
191
|
+
"File path within the repository (e.g., \"src/server/index.ts\")"
|
|
192
|
+
),
|
|
193
|
+
},
|
|
194
|
+
"startLine": {
|
|
195
|
+
"type": "number",
|
|
196
|
+
"description": "Start line number (1-based). Omit to start from beginning.",
|
|
197
|
+
},
|
|
198
|
+
"endLine": {
|
|
199
|
+
"type": "number",
|
|
200
|
+
"description": "End line number (1-based, inclusive). Omit to read to the end.",
|
|
201
|
+
},
|
|
202
|
+
"branch": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"description": (
|
|
205
|
+
"Branch to read from (defaults to the repository default branch)"
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
"required": ["github", "path"],
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def browser_tool_def(*, description: str | None = None) -> dict[str, Any]:
|
|
215
|
+
"""Generate an Anthropic Tool dict for ``browser_task``."""
|
|
216
|
+
return {
|
|
217
|
+
"name": "browser_task",
|
|
218
|
+
"description": description or BROWSER_TOOL_DESCRIPTION,
|
|
219
|
+
"input_schema": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"properties": {
|
|
222
|
+
"task": {
|
|
223
|
+
"type": "string",
|
|
224
|
+
"description": (
|
|
225
|
+
"Natural language description of what to do "
|
|
226
|
+
"(e.g., \"Test checkout flow for buying a pineapple\")"
|
|
227
|
+
),
|
|
228
|
+
},
|
|
229
|
+
"url": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"description": (
|
|
232
|
+
"Starting URL (e.g., https://3000-xyz.e2b.dev). "
|
|
233
|
+
"Required if navigating to a specific page."
|
|
234
|
+
),
|
|
235
|
+
},
|
|
236
|
+
"maxSteps": {
|
|
237
|
+
"type": "number",
|
|
238
|
+
"description": (
|
|
239
|
+
"Maximum number of browser actions to take (1-50). "
|
|
240
|
+
"Default: 10. Use 15-30 for complex flows."
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
"region": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"enum": ["sfo", "lon"],
|
|
246
|
+
"description": (
|
|
247
|
+
"Browserless region: sfo (US West Coast) or lon (Europe). "
|
|
248
|
+
"Default: sfo."
|
|
249
|
+
),
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
"required": ["task"],
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def mobile_tool_def(*, description: str | None = None) -> dict[str, Any]:
|
|
258
|
+
"""Generate an Anthropic Tool dict for ``mobile_task``."""
|
|
259
|
+
return {
|
|
260
|
+
"name": "mobile_task",
|
|
261
|
+
"description": description or MOBILE_TOOL_DESCRIPTION,
|
|
262
|
+
"input_schema": {
|
|
263
|
+
"type": "object",
|
|
264
|
+
"properties": {
|
|
265
|
+
"task": {
|
|
266
|
+
"type": "string",
|
|
267
|
+
"description": "Natural language description of what to do in the mobile app",
|
|
268
|
+
},
|
|
269
|
+
"app": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"description": "Application identifier (bundle ID or BrowserStack bs:// URL)",
|
|
272
|
+
},
|
|
273
|
+
"platform": {
|
|
274
|
+
"type": "string",
|
|
275
|
+
"enum": ["ios", "android"],
|
|
276
|
+
"description": "Target platform (default: ios)",
|
|
277
|
+
},
|
|
278
|
+
"max_steps": {
|
|
279
|
+
"type": "number",
|
|
280
|
+
"description": "Maximum agent steps (default: 50)",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
"required": ["task", "app"],
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# AnthropicTool -- pairs a definition with an executor
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
class AnthropicTool:
|
|
293
|
+
"""A tool definition paired with an executor for Anthropic messages.
|
|
294
|
+
|
|
295
|
+
Behaves like a dict (can be passed directly to ``tools=[tool]``) while
|
|
296
|
+
also exposing ``execute()`` and ``format_result()`` methods.
|
|
297
|
+
|
|
298
|
+
Example::
|
|
299
|
+
|
|
300
|
+
tool = morph.edit.as_anthropic_tool(base_dir="./src")
|
|
301
|
+
|
|
302
|
+
# Pass to Anthropic
|
|
303
|
+
response = client.messages.create(
|
|
304
|
+
model="claude-sonnet-4-5-20250929",
|
|
305
|
+
tools=[tool],
|
|
306
|
+
messages=[...],
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Execute
|
|
310
|
+
result = tool.execute(tool_use_block.input)
|
|
311
|
+
formatted = tool.format_result(result)
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
definition: dict[str, Any],
|
|
317
|
+
executor: Callable[..., Any],
|
|
318
|
+
formatter: Callable[..., str],
|
|
319
|
+
system_prompt: str = "",
|
|
320
|
+
) -> None:
|
|
321
|
+
self._definition = definition
|
|
322
|
+
self._executor = executor
|
|
323
|
+
self._formatter = formatter
|
|
324
|
+
self.system_prompt = system_prompt
|
|
325
|
+
|
|
326
|
+
# -- dict protocol so Anthropic SDK accepts this as a tool ---
|
|
327
|
+
def __getitem__(self, key: str) -> Any:
|
|
328
|
+
return self._definition[key]
|
|
329
|
+
|
|
330
|
+
def __contains__(self, key: str) -> bool:
|
|
331
|
+
return key in self._definition
|
|
332
|
+
|
|
333
|
+
def __iter__(self) -> Iterator[str]:
|
|
334
|
+
return iter(self._definition)
|
|
335
|
+
|
|
336
|
+
def keys(self) -> KeysView[str]:
|
|
337
|
+
return self._definition.keys()
|
|
338
|
+
|
|
339
|
+
def items(self) -> ItemsView[str, Any]:
|
|
340
|
+
return self._definition.items()
|
|
341
|
+
|
|
342
|
+
def values(self) -> ValuesView[Any]:
|
|
343
|
+
return self._definition.values()
|
|
344
|
+
|
|
345
|
+
def to_dict(self) -> dict[str, Any]:
|
|
346
|
+
"""Return the raw tool definition dict."""
|
|
347
|
+
return self._definition
|
|
348
|
+
|
|
349
|
+
# -- execution ---
|
|
350
|
+
def execute(self, input: dict[str, Any] | str) -> Any:
|
|
351
|
+
"""Execute the tool with the given input.
|
|
352
|
+
|
|
353
|
+
*input* may be a dict (from ``tool_use.input``) or a JSON string.
|
|
354
|
+
"""
|
|
355
|
+
args: dict[str, Any] = json.loads(input) if isinstance(input, str) else input
|
|
356
|
+
return self._executor(**args)
|
|
357
|
+
|
|
358
|
+
def format_result(self, result: Any) -> str:
|
|
359
|
+
"""Format a result for passing back as a tool_result content block."""
|
|
360
|
+
return self._formatter(result)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""LangChain framework adapter -- generates LangChain-compatible tool instances.
|
|
2
|
+
|
|
3
|
+
Requires ``langchain-core`` as an optional dependency. All imports are
|
|
4
|
+
deferred so the adapter module can be imported even when LangChain is not
|
|
5
|
+
installed; a clear error is raised at tool-creation time if the dependency
|
|
6
|
+
is missing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ensure_langchain() -> None:
|
|
18
|
+
"""Raise a helpful error if langchain-core is not installed."""
|
|
19
|
+
try:
|
|
20
|
+
import langchain_core # noqa: F401
|
|
21
|
+
except ImportError:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"LangChain adapter requires 'langchain-core'. "
|
|
24
|
+
"Install it with: pip install langchain-core"
|
|
25
|
+
) from None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_tool(
|
|
29
|
+
*,
|
|
30
|
+
name: str,
|
|
31
|
+
description: str,
|
|
32
|
+
args_schema: dict[str, Any],
|
|
33
|
+
executor: Callable[..., Any],
|
|
34
|
+
formatter: Callable[..., str] | None = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Create a LangChain ``StructuredTool`` from a Morph tool specification.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
name:
|
|
41
|
+
Tool name (e.g. ``edit_file``).
|
|
42
|
+
description:
|
|
43
|
+
Tool description shown to the LLM.
|
|
44
|
+
args_schema:
|
|
45
|
+
JSON Schema dict describing the tool parameters.
|
|
46
|
+
executor:
|
|
47
|
+
Callable that executes the tool. Receives kwargs matching the schema.
|
|
48
|
+
formatter:
|
|
49
|
+
Optional callable to format the result as a string.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
A ``langchain_core.tools.StructuredTool`` instance.
|
|
54
|
+
"""
|
|
55
|
+
_ensure_langchain()
|
|
56
|
+
|
|
57
|
+
from langchain_core.tools import StructuredTool
|
|
58
|
+
|
|
59
|
+
def _run(**kwargs: Any) -> str:
|
|
60
|
+
result = executor(**kwargs)
|
|
61
|
+
if formatter is not None:
|
|
62
|
+
return formatter(result)
|
|
63
|
+
return str(result)
|
|
64
|
+
|
|
65
|
+
# Build a Pydantic model from the JSON schema for LangChain's args_schema
|
|
66
|
+
input_model = _json_schema_to_pydantic(name, args_schema)
|
|
67
|
+
|
|
68
|
+
return StructuredTool(
|
|
69
|
+
name=name,
|
|
70
|
+
description=description,
|
|
71
|
+
func=_run,
|
|
72
|
+
args_schema=input_model,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _json_schema_to_pydantic(tool_name: str, schema: dict[str, Any]) -> type[BaseModel]:
|
|
77
|
+
"""Convert a flat JSON Schema ``properties`` dict to a Pydantic model.
|
|
78
|
+
|
|
79
|
+
This handles the common case of top-level string/number/array properties.
|
|
80
|
+
Nested objects are left as ``dict``.
|
|
81
|
+
"""
|
|
82
|
+
_ensure_langchain()
|
|
83
|
+
|
|
84
|
+
from pydantic import Field, create_model
|
|
85
|
+
|
|
86
|
+
properties = schema.get("properties", {})
|
|
87
|
+
required = set(schema.get("required", []))
|
|
88
|
+
field_definitions: dict[str, Any] = {}
|
|
89
|
+
|
|
90
|
+
_TYPE_MAP = {
|
|
91
|
+
"string": str,
|
|
92
|
+
"number": float,
|
|
93
|
+
"integer": int,
|
|
94
|
+
"boolean": bool,
|
|
95
|
+
"object": dict,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for prop_name, prop_schema in properties.items():
|
|
99
|
+
prop_type = prop_schema.get("type", "string")
|
|
100
|
+
description = prop_schema.get("description", "")
|
|
101
|
+
|
|
102
|
+
python_type: type[Any]
|
|
103
|
+
if prop_type == "array":
|
|
104
|
+
item_type = _TYPE_MAP.get(
|
|
105
|
+
prop_schema.get("items", {}).get("type", "string"), str
|
|
106
|
+
)
|
|
107
|
+
python_type = list[item_type] # type: ignore[valid-type]
|
|
108
|
+
else:
|
|
109
|
+
python_type = _TYPE_MAP.get(prop_type, str)
|
|
110
|
+
|
|
111
|
+
if prop_name in required:
|
|
112
|
+
field_definitions[prop_name] = (python_type, Field(description=description))
|
|
113
|
+
else:
|
|
114
|
+
field_definitions[prop_name] = (
|
|
115
|
+
Optional[python_type],
|
|
116
|
+
Field(default=None, description=description),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
model_name = f"{tool_name.title().replace('_', '')}Input"
|
|
120
|
+
return cast("type[BaseModel]", create_model(model_name, **field_definitions))
|