universal-mcp 0.1.20rc2__py3-none-any.whl → 0.1.22rc1__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.
- universal_mcp/applications/__init__.py +0 -1
- universal_mcp/integrations/integration.py +4 -8
- universal_mcp/servers/server.py +36 -30
- universal_mcp/tools/adapters.py +21 -0
- universal_mcp/tools/func_metadata.py +4 -2
- universal_mcp/tools/manager.py +122 -37
- universal_mcp/utils/agentr.py +3 -13
- universal_mcp/utils/docstring_parser.py +18 -64
- universal_mcp/utils/openapi/api_splitter.py +250 -132
- universal_mcp/utils/openapi/openapi.py +221 -98
- universal_mcp/utils/openapi/preprocessor.py +272 -29
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/METADATA +2 -1
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/RECORD +16 -16
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,11 @@
|
|
1
1
|
import ast
|
2
2
|
import re
|
3
|
+
import subprocess
|
3
4
|
from collections import defaultdict
|
4
5
|
from keyword import iskeyword
|
5
6
|
from pathlib import Path
|
6
7
|
|
7
|
-
API_SEGMENT_BASE_CODE =
|
8
|
+
API_SEGMENT_BASE_CODE = """
|
8
9
|
from typing import Any
|
9
10
|
|
10
11
|
class APISegmentBase:
|
@@ -25,7 +26,8 @@ class APISegmentBase:
|
|
25
26
|
|
26
27
|
def _delete(self, url: str, params: dict = None, **kwargs):
|
27
28
|
return self.main_app_client._delete(url, params=params, **kwargs)
|
28
|
-
|
29
|
+
"""
|
30
|
+
|
29
31
|
|
30
32
|
def get_sanitized_path_segment(openapi_path: str) -> str:
|
31
33
|
# Remove leading/trailing slashes and split
|
@@ -35,13 +37,13 @@ def get_sanitized_path_segment(openapi_path: str) -> str:
|
|
35
37
|
return "default_api"
|
36
38
|
|
37
39
|
# Handle common prefixes like /api/ or /v1/api/ etc.
|
38
|
-
known_prefixes = ["api"]
|
40
|
+
known_prefixes = ["api"]
|
39
41
|
while len(path_parts) > 0 and path_parts[0].lower() in known_prefixes:
|
40
42
|
path_parts.pop(0)
|
41
|
-
if not path_parts:
|
42
|
-
return "default_api"
|
43
|
+
if not path_parts:
|
44
|
+
return "default_api"
|
43
45
|
|
44
|
-
segment_to_use = "default_api"
|
46
|
+
segment_to_use = "default_api"
|
45
47
|
|
46
48
|
# check if the current first segment is version-like (e.g., "2", "v1", "v0")
|
47
49
|
if path_parts:
|
@@ -56,74 +58,73 @@ def get_sanitized_path_segment(openapi_path: str) -> str:
|
|
56
58
|
# If it's not a version segment, use it directly
|
57
59
|
segment_to_use = path_parts[0]
|
58
60
|
else:
|
59
|
-
|
60
61
|
segment_to_use = f"api_{first_segment}"
|
61
|
-
else:
|
62
|
-
return "default_api"
|
62
|
+
else: # Path was empty after stripping prefixes (e.g. "/api/")
|
63
|
+
return "default_api" # segment_to_use remains "default_api"
|
63
64
|
|
64
65
|
# Sanitize the chosen segment to be a valid Python identifier component
|
65
66
|
# Replace non-alphanumeric (excluding underscore) with underscore
|
66
67
|
sanitized_segment = re.sub(r"[^a-zA-Z0-9_]", "_", segment_to_use)
|
67
|
-
|
68
|
+
|
68
69
|
# Remove leading/trailing underscores that might result from sanitization
|
69
70
|
sanitized_segment = sanitized_segment.strip("_")
|
70
71
|
|
71
|
-
|
72
72
|
if not sanitized_segment:
|
73
73
|
return "default_api"
|
74
|
-
if sanitized_segment.isdigit():
|
74
|
+
if sanitized_segment.isdigit(): # e.g. if path was /2/123 -> segment is 123
|
75
75
|
return f"api_{sanitized_segment}"
|
76
|
-
|
76
|
+
|
77
77
|
return sanitized_segment
|
78
78
|
|
79
|
+
|
79
80
|
def get_group_name_from_path(openapi_path: str) -> str:
|
80
81
|
processed_path = openapi_path
|
81
|
-
|
82
|
+
|
82
83
|
# Pattern for /vN, /vN.N, /vN.N.N
|
83
84
|
version_pattern_v_prefix = re.compile(r"^/v[0-9]+(?:\\.[0-9]+){0,2}")
|
84
85
|
# Pattern for just /N (like /2)
|
85
86
|
version_pattern_numeric_prefix = re.compile(r"^/[0-9]+")
|
86
|
-
|
87
|
+
|
87
88
|
api_prefix_pattern = re.compile(r"^/api")
|
88
89
|
|
89
90
|
# Strip /api prefix first if present
|
90
91
|
if api_prefix_pattern.match(processed_path):
|
91
92
|
processed_path = api_prefix_pattern.sub("", processed_path)
|
92
|
-
processed_path = processed_path.lstrip("/")
|
93
|
+
processed_path = processed_path.lstrip("/") # Ensure we strip leading slash if api was the only thing
|
93
94
|
if processed_path and not processed_path.startswith("/"):
|
94
|
-
|
95
|
-
elif not processed_path:
|
96
|
-
processed_path = "/"
|
95
|
+
processed_path = "/" + processed_path
|
96
|
+
elif not processed_path: # Path was only /api/
|
97
|
+
processed_path = "/" # Reset to / so subsequent logic doesn't fail
|
97
98
|
|
98
99
|
# Try to strip /vN style version
|
99
100
|
path_after_v_version_strip = version_pattern_v_prefix.sub("", processed_path)
|
100
|
-
|
101
|
-
if path_after_v_version_strip != processed_path:
|
101
|
+
|
102
|
+
if path_after_v_version_strip != processed_path: # /vN was stripped
|
102
103
|
processed_path = path_after_v_version_strip
|
103
|
-
else:
|
104
|
+
else: # /vN was not found, try to strip /N (like /2/)
|
104
105
|
path_after_numeric_version_strip = version_pattern_numeric_prefix.sub("", processed_path)
|
105
|
-
if path_after_numeric_version_strip != processed_path:
|
106
|
+
if path_after_numeric_version_strip != processed_path: # /N was stripped
|
106
107
|
processed_path = path_after_numeric_version_strip
|
107
|
-
|
108
|
+
|
108
109
|
processed_path = processed_path.lstrip("/")
|
109
110
|
|
110
|
-
path_segments = [segment for segment in processed_path.split("/") if segment]
|
111
|
+
path_segments = [segment for segment in processed_path.split("/") if segment] # Ensure no empty segments
|
111
112
|
|
112
113
|
group_name_raw = "default"
|
113
114
|
if path_segments and path_segments[0]:
|
114
115
|
# Remove {param} style parts from the segment if any
|
115
|
-
group_name_raw = re.sub(r"[{}]", "", path_segments[0])
|
116
|
-
|
116
|
+
group_name_raw = re.sub(r"[{}]", "", path_segments[0]) # Corrected regex string
|
117
|
+
|
117
118
|
# Sanitize to make it a valid Python identifier component (lowercase, underscores)
|
118
|
-
group_name = re.sub(r"[^a-zA-Z0-9_]", "_", group_name_raw).lower()
|
119
|
+
group_name = re.sub(r"[^a-zA-Z0-9_]", "_", group_name_raw).lower() # Corrected regex string
|
119
120
|
group_name = group_name.strip("_")
|
120
121
|
|
121
|
-
if not group_name or group_name.isdigit():
|
122
|
+
if not group_name or group_name.isdigit(): # If empty after sanitization or purely numeric
|
122
123
|
group_name = f"api_{group_name}" if group_name else "default_api"
|
123
|
-
|
124
|
-
if iskeyword(group_name):
|
124
|
+
|
125
|
+
if iskeyword(group_name): # Avoid Python keywords
|
125
126
|
group_name += "_"
|
126
|
-
|
127
|
+
|
127
128
|
return group_name if group_name else "default_api"
|
128
129
|
|
129
130
|
|
@@ -137,11 +138,11 @@ class MethodTransformer(ast.NodeTransformer):
|
|
137
138
|
return node
|
138
139
|
|
139
140
|
def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
|
140
|
-
if isinstance(node.value, ast.Name) and node.value.id ==
|
141
|
+
if isinstance(node.value, ast.Name) and node.value.id == "self" and node.attr == "base_url":
|
141
142
|
return ast.Attribute(
|
142
|
-
value=ast.Attribute(value=ast.Name(id=
|
143
|
-
attr=
|
144
|
-
ctx=ast.Load()
|
143
|
+
value=ast.Attribute(value=ast.Name(id="self", ctx=ast.Load()), attr="main_app_client", ctx=ast.Load()),
|
144
|
+
attr="base_url",
|
145
|
+
ctx=ast.Load(),
|
145
146
|
)
|
146
147
|
return self.generic_visit(node)
|
147
148
|
|
@@ -154,78 +155,86 @@ def split_generated_app_file(input_app_file: Path, output_dir: Path):
|
|
154
155
|
tree = ast.parse(content)
|
155
156
|
|
156
157
|
main_app_class_node = None
|
157
|
-
for node_item in tree.body:
|
158
|
-
if isinstance(node_item, ast.ClassDef) and
|
159
|
-
|
158
|
+
for node_item in tree.body: # Renamed to avoid conflict with ast.Node
|
159
|
+
if isinstance(node_item, ast.ClassDef) and any(
|
160
|
+
isinstance(base, ast.Name) and base.id == "APIApplication"
|
161
|
+
for base in node_item.bases
|
162
|
+
if isinstance(base, ast.Name)
|
163
|
+
):
|
160
164
|
main_app_class_node = node_item
|
161
165
|
break
|
162
|
-
|
166
|
+
|
163
167
|
if not main_app_class_node:
|
164
168
|
raise ValueError("Could not find main APIApplication class in the input file.")
|
165
169
|
|
166
170
|
grouped_methods = defaultdict(list)
|
167
171
|
other_main_app_body_nodes = []
|
168
172
|
processed_method_names_in_main = set()
|
169
|
-
|
173
|
+
|
170
174
|
openapi_path_regex = re.compile(r"# openapi_path: (.+)")
|
171
175
|
|
172
176
|
for item in main_app_class_node.body:
|
173
177
|
if isinstance(item, ast.FunctionDef):
|
174
178
|
path_from_comment = None
|
175
|
-
if
|
176
|
-
|
179
|
+
if (
|
180
|
+
item.body
|
181
|
+
and isinstance(item.body[0], ast.Expr)
|
182
|
+
and isinstance(item.body[0].value, ast.Constant)
|
183
|
+
and isinstance(item.body[0].value.value, str)
|
184
|
+
):
|
177
185
|
docstring_lines = item.body[0].value.value.strip().splitlines()
|
178
186
|
if docstring_lines:
|
179
187
|
match = openapi_path_regex.match(docstring_lines[0].strip())
|
180
188
|
if match:
|
181
189
|
path_from_comment = match.group(1).strip()
|
182
|
-
|
190
|
+
|
183
191
|
if path_from_comment:
|
184
192
|
group = get_group_name_from_path(path_from_comment)
|
185
193
|
method_node_copy = ast.parse(ast.unparse(item)).body[0]
|
186
194
|
if not isinstance(method_node_copy, ast.FunctionDef):
|
187
|
-
|
195
|
+
method_node_copy = item
|
188
196
|
|
189
197
|
transformer = MethodTransformer(original_path=path_from_comment)
|
190
198
|
transformed_method_node = transformer.visit(method_node_copy)
|
191
|
-
if hasattr(ast,
|
199
|
+
if hasattr(ast, "fix_missing_locations"):
|
192
200
|
transformed_method_node = ast.fix_missing_locations(transformed_method_node)
|
193
201
|
|
194
202
|
# Remove the # openapi_path: comment from the docstring
|
195
|
-
if (
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
203
|
+
if (
|
204
|
+
transformed_method_node.body
|
205
|
+
and isinstance(transformed_method_node.body[0], ast.Expr)
|
206
|
+
and isinstance(transformed_method_node.body[0].value, ast.Constant)
|
207
|
+
and isinstance(transformed_method_node.body[0].value.value, str)
|
208
|
+
):
|
200
209
|
docstring_expr_node = transformed_method_node.body[0]
|
201
210
|
original_docstring_text = docstring_expr_node.value.value
|
202
|
-
|
211
|
+
|
203
212
|
all_lines_raw = original_docstring_text.splitlines(True)
|
204
213
|
line_to_remove_idx = -1
|
205
|
-
|
214
|
+
|
206
215
|
for i, current_line_raw in enumerate(all_lines_raw):
|
207
216
|
current_line_stripped = current_line_raw.strip()
|
208
|
-
if current_line_stripped:
|
217
|
+
if current_line_stripped: # Found the first significant line
|
209
218
|
if openapi_path_regex.match(current_line_stripped):
|
210
219
|
line_to_remove_idx = i
|
211
|
-
break
|
220
|
+
break # Only inspect the first significant line
|
212
221
|
|
213
222
|
if line_to_remove_idx != -1:
|
214
223
|
del all_lines_raw[line_to_remove_idx]
|
215
224
|
modified_docstring_text = "".join(all_lines_raw)
|
216
|
-
|
225
|
+
|
217
226
|
if not modified_docstring_text.strip():
|
218
|
-
transformed_method_node.body.pop(0)
|
227
|
+
transformed_method_node.body.pop(0) # Docstring is now empty
|
219
228
|
else:
|
220
229
|
docstring_expr_node.value.value = modified_docstring_text
|
221
|
-
|
230
|
+
|
222
231
|
grouped_methods[group].append(transformed_method_node)
|
223
232
|
processed_method_names_in_main.add(item.name)
|
224
233
|
else:
|
225
234
|
other_main_app_body_nodes.append(item)
|
226
235
|
else:
|
227
236
|
other_main_app_body_nodes.append(item)
|
228
|
-
|
237
|
+
|
229
238
|
# Define segments subfolder
|
230
239
|
segments_foldername = "api_segments"
|
231
240
|
segments_dir = output_dir / segments_foldername
|
@@ -238,126 +247,222 @@ def split_generated_app_file(input_app_file: Path, output_dir: Path):
|
|
238
247
|
for group, method_nodes in grouped_methods.items():
|
239
248
|
SegmentClassName = "".join(word.capitalize() for word in group.split("_")) + "Api"
|
240
249
|
segment_filename = f"{group.lower()}_api.py"
|
241
|
-
|
250
|
+
|
242
251
|
method_names_for_list_tools = [method_node.name for method_node in method_nodes]
|
243
252
|
|
244
|
-
list_tools_body = [
|
245
|
-
|
246
|
-
|
247
|
-
|
253
|
+
list_tools_body = [
|
254
|
+
ast.Return(
|
255
|
+
value=ast.List(
|
256
|
+
elts=[
|
257
|
+
ast.Attribute(value=ast.Name(id="self", ctx=ast.Load()), attr=name, ctx=ast.Load())
|
258
|
+
for name in method_names_for_list_tools
|
259
|
+
],
|
260
|
+
ctx=ast.Load(),
|
261
|
+
)
|
262
|
+
)
|
263
|
+
]
|
248
264
|
list_tools_def = ast.FunctionDef(
|
249
|
-
name=
|
250
|
-
args=ast.arguments(
|
251
|
-
|
265
|
+
name="list_tools",
|
266
|
+
args=ast.arguments(
|
267
|
+
posonlyargs=[],
|
268
|
+
args=[ast.arg(arg="self")],
|
269
|
+
vararg=None,
|
270
|
+
kwonlyargs=[],
|
271
|
+
kw_defaults=[],
|
272
|
+
kwarg=None,
|
273
|
+
defaults=[],
|
274
|
+
),
|
275
|
+
body=list_tools_body,
|
276
|
+
decorator_list=[],
|
277
|
+
returns=None,
|
252
278
|
)
|
253
|
-
|
279
|
+
|
254
280
|
init_method_segment = ast.FunctionDef(
|
255
|
-
name=
|
281
|
+
name="__init__",
|
256
282
|
args=ast.arguments(
|
257
283
|
posonlyargs=[],
|
258
|
-
args=[
|
259
|
-
|
284
|
+
args=[
|
285
|
+
ast.arg(arg="self"),
|
286
|
+
ast.arg(arg="main_app_client", annotation=ast.Name(id="Any", ctx=ast.Load())),
|
287
|
+
],
|
288
|
+
vararg=None,
|
289
|
+
kwonlyargs=[],
|
290
|
+
kw_defaults=[],
|
291
|
+
kwarg=None,
|
292
|
+
defaults=[],
|
260
293
|
),
|
261
294
|
body=[
|
262
|
-
ast.Expr(
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
295
|
+
ast.Expr(
|
296
|
+
value=ast.Call(
|
297
|
+
func=ast.Attribute(
|
298
|
+
value=ast.Call(func=ast.Name(id="super", ctx=ast.Load()), args=[], keywords=[]),
|
299
|
+
attr="__init__",
|
300
|
+
ctx=ast.Load(),
|
301
|
+
),
|
302
|
+
args=[ast.Name(id="main_app_client", ctx=ast.Load())],
|
303
|
+
keywords=[],
|
304
|
+
)
|
305
|
+
)
|
267
306
|
],
|
268
|
-
decorator_list=[],
|
307
|
+
decorator_list=[],
|
308
|
+
returns=None,
|
269
309
|
)
|
270
310
|
|
271
311
|
segment_class_body = [init_method_segment] + method_nodes + [list_tools_def]
|
272
312
|
segment_class_node = ast.ClassDef(
|
273
313
|
name=SegmentClassName,
|
274
|
-
bases=[ast.Name(id=
|
275
|
-
keywords=[],
|
314
|
+
bases=[ast.Name(id="APISegmentBase", ctx=ast.Load())],
|
315
|
+
keywords=[],
|
316
|
+
body=segment_class_body,
|
317
|
+
decorator_list=[],
|
276
318
|
)
|
277
|
-
|
319
|
+
|
278
320
|
segment_module_body = [
|
279
|
-
ast.ImportFrom(
|
280
|
-
|
281
|
-
|
321
|
+
ast.ImportFrom(
|
322
|
+
module="typing",
|
323
|
+
names=[ast.alias(name="Any"), ast.alias(name="List"), ast.alias(name="Optional")],
|
324
|
+
level=0,
|
325
|
+
),
|
326
|
+
ast.ImportFrom(
|
327
|
+
module=".api_segment_base", names=[ast.alias(name="APISegmentBase")], level=0
|
328
|
+
), # This relative import is fine as they are in the same dir
|
329
|
+
segment_class_node,
|
282
330
|
]
|
283
331
|
segment_module_ast = ast.Module(body=segment_module_body, type_ignores=[])
|
284
|
-
if hasattr(ast,
|
332
|
+
if hasattr(ast, "fix_missing_locations"):
|
285
333
|
segment_module_ast = ast.fix_missing_locations(segment_module_ast)
|
286
|
-
|
334
|
+
|
287
335
|
(segments_dir / segment_filename).write_text(ast.unparse(segment_module_ast))
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
336
|
+
# Format the generated segment file with black
|
337
|
+
subprocess.run(["black", str(segments_dir / segment_filename)])
|
338
|
+
segment_class_details_for_main_app.append(
|
339
|
+
{
|
340
|
+
"attr_name": group.lower(),
|
341
|
+
"class_name": SegmentClassName,
|
342
|
+
"module_name": segment_filename.replace(".py", ""), # Used for import in main app
|
343
|
+
}
|
344
|
+
)
|
293
345
|
|
294
346
|
new_main_app_body = []
|
295
347
|
main_app_init_node = None
|
296
348
|
|
297
349
|
for node in other_main_app_body_nodes:
|
298
350
|
if isinstance(node, ast.FunctionDef):
|
299
|
-
if node.name ==
|
351
|
+
if node.name == "__init__":
|
300
352
|
main_app_init_node = node
|
301
353
|
continue
|
302
|
-
elif node.name ==
|
354
|
+
elif node.name == "list_tools":
|
303
355
|
continue
|
304
356
|
new_main_app_body.append(node)
|
305
357
|
|
306
358
|
if not main_app_init_node:
|
307
359
|
main_app_init_node = ast.FunctionDef(
|
308
|
-
name=
|
360
|
+
name="__init__",
|
309
361
|
args=ast.arguments(
|
310
|
-
posonlyargs=[],
|
311
|
-
args=[
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
362
|
+
posonlyargs=[],
|
363
|
+
args=[
|
364
|
+
ast.arg(arg="self"),
|
365
|
+
ast.arg(
|
366
|
+
arg="integration",
|
367
|
+
annotation=ast.Name(id="Integration", ctx=ast.Load()),
|
368
|
+
default=ast.Constant(value=None),
|
369
|
+
),
|
370
|
+
],
|
371
|
+
vararg=ast.arg(arg="args"),
|
372
|
+
kwonlyargs=[],
|
373
|
+
kw_defaults=[],
|
374
|
+
kwarg=ast.arg(arg="kwargs"),
|
375
|
+
defaults=[ast.Constant(value=None)],
|
316
376
|
),
|
317
377
|
body=[
|
318
|
-
ast.Expr(
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
378
|
+
ast.Expr(
|
379
|
+
value=ast.Call(
|
380
|
+
func=ast.Attribute(
|
381
|
+
value=ast.Call(func=ast.Name(id="super", ctx=ast.Load()), args=[], keywords=[]),
|
382
|
+
attr="__init__",
|
383
|
+
ctx=ast.Load(),
|
384
|
+
),
|
385
|
+
args=[
|
386
|
+
ast.keyword(arg="name", value=ast.Constant(value=main_app_class_node.name.lower())),
|
387
|
+
ast.Name(id="integration", ctx=ast.Load()),
|
388
|
+
],
|
389
|
+
keywords=[ast.keyword(arg=None, value=ast.Name(id="kwargs", ctx=ast.Load()))],
|
390
|
+
)
|
391
|
+
)
|
323
392
|
],
|
324
|
-
decorator_list=[],
|
393
|
+
decorator_list=[],
|
394
|
+
returns=ast.Constant(value=None),
|
325
395
|
)
|
326
396
|
|
327
397
|
init_segment_instantiations = []
|
328
398
|
for seg_detail in segment_class_details_for_main_app:
|
329
399
|
init_segment_instantiations.append(
|
330
400
|
ast.Assign(
|
331
|
-
targets=[
|
332
|
-
|
401
|
+
targets=[
|
402
|
+
ast.Attribute(
|
403
|
+
value=ast.Name(id="self", ctx=ast.Load()), attr=seg_detail["attr_name"], ctx=ast.Store()
|
404
|
+
)
|
405
|
+
],
|
406
|
+
value=ast.Call(
|
407
|
+
func=ast.Name(id=seg_detail["class_name"], ctx=ast.Load()),
|
408
|
+
args=[ast.Name(id="self", ctx=ast.Load())],
|
409
|
+
keywords=[],
|
410
|
+
),
|
333
411
|
)
|
334
412
|
)
|
335
413
|
if not isinstance(main_app_init_node.body, list):
|
336
|
-
main_app_init_node.body = [main_app_init_node.body]
|
337
|
-
|
414
|
+
main_app_init_node.body = [main_app_init_node.body] # type: ignore
|
415
|
+
|
338
416
|
main_app_init_node.body.extend(init_segment_instantiations)
|
339
417
|
new_main_app_body.insert(0, main_app_init_node)
|
340
418
|
|
341
419
|
list_tools_calls_for_main = [
|
342
420
|
ast.Call(
|
343
|
-
func=ast.Attribute(
|
344
|
-
|
345
|
-
|
421
|
+
func=ast.Attribute(
|
422
|
+
value=ast.Attribute(
|
423
|
+
value=ast.Name(id="self", ctx=ast.Load()), attr=seg_detail["attr_name"], ctx=ast.Load()
|
424
|
+
),
|
425
|
+
attr="list_tools",
|
426
|
+
ctx=ast.Load(),
|
427
|
+
),
|
428
|
+
args=[],
|
429
|
+
keywords=[],
|
430
|
+
)
|
431
|
+
for seg_detail in segment_class_details_for_main_app
|
432
|
+
]
|
433
|
+
|
434
|
+
new_list_tools_body = [
|
435
|
+
ast.Assign(targets=[ast.Name(id="all_tools", ctx=ast.Store())], value=ast.List(elts=[], ctx=ast.Load()))
|
346
436
|
]
|
347
|
-
|
348
|
-
new_list_tools_body = [ast.Assign(targets=[ast.Name(id='all_tools', ctx=ast.Store())], value=ast.List(elts=[], ctx=ast.Load()))]
|
349
437
|
if list_tools_calls_for_main:
|
350
438
|
for call_node in list_tools_calls_for_main:
|
351
|
-
new_list_tools_body.append(
|
352
|
-
|
353
|
-
|
439
|
+
new_list_tools_body.append(
|
440
|
+
ast.Expr(
|
441
|
+
value=ast.Call(
|
442
|
+
func=ast.Attribute(
|
443
|
+
value=ast.Name(id="all_tools", ctx=ast.Load()), attr="extend", ctx=ast.Load()
|
444
|
+
),
|
445
|
+
args=[call_node],
|
446
|
+
keywords=[],
|
447
|
+
)
|
448
|
+
)
|
354
449
|
)
|
355
|
-
new_list_tools_body.append(ast.Return(value=ast.Name(id=
|
450
|
+
new_list_tools_body.append(ast.Return(value=ast.Name(id="all_tools", ctx=ast.Load())))
|
356
451
|
|
357
452
|
new_main_app_list_tools_def = ast.FunctionDef(
|
358
|
-
name=
|
359
|
-
args=ast.arguments(
|
360
|
-
|
453
|
+
name="list_tools",
|
454
|
+
args=ast.arguments(
|
455
|
+
posonlyargs=[],
|
456
|
+
args=[ast.arg(arg="self")],
|
457
|
+
vararg=None,
|
458
|
+
kwonlyargs=[],
|
459
|
+
kw_defaults=[],
|
460
|
+
kwarg=None,
|
461
|
+
defaults=[],
|
462
|
+
),
|
463
|
+
body=new_list_tools_body,
|
464
|
+
decorator_list=[],
|
465
|
+
returns=None,
|
361
466
|
)
|
362
467
|
new_main_app_body.append(new_main_app_list_tools_def)
|
363
468
|
|
@@ -373,28 +478,41 @@ def split_generated_app_file(input_app_file: Path, output_dir: Path):
|
|
373
478
|
if isinstance(top_node, ast.Import | ast.ImportFrom):
|
374
479
|
final_main_module_imports.append(top_node)
|
375
480
|
if isinstance(top_node, ast.ImportFrom):
|
376
|
-
if top_node.module:
|
377
|
-
|
481
|
+
if top_node.module: # module can be None for from . import x
|
482
|
+
original_imports_from_main_file.add(top_node.module)
|
378
483
|
elif isinstance(top_node, ast.Import):
|
379
|
-
|
380
|
-
|
484
|
+
for alias in top_node.names:
|
485
|
+
original_imports_from_main_file.add(alias.name)
|
381
486
|
else:
|
382
487
|
other_top_level_nodes_for_main.append(top_node)
|
383
|
-
|
488
|
+
|
384
489
|
if "typing" not in original_imports_from_main_file:
|
385
|
-
|
490
|
+
final_main_module_imports.insert(
|
491
|
+
0,
|
492
|
+
ast.ImportFrom(
|
493
|
+
module="typing",
|
494
|
+
names=[ast.alias(name="Any"), ast.alias(name="Dict"), ast.alias(name="Optional")],
|
495
|
+
level=0,
|
496
|
+
),
|
497
|
+
)
|
386
498
|
|
387
499
|
for seg_detail in segment_class_details_for_main_app:
|
388
500
|
# Adjust import path for segments subfolder
|
389
501
|
final_main_module_imports.append(
|
390
|
-
ast.ImportFrom(
|
502
|
+
ast.ImportFrom(
|
503
|
+
module=f".{segments_foldername}.{seg_detail['module_name']}",
|
504
|
+
names=[ast.alias(name=seg_detail["class_name"])],
|
505
|
+
level=0,
|
506
|
+
)
|
391
507
|
)
|
392
|
-
|
393
|
-
final_main_app_module_ast = ast.Module(
|
394
|
-
|
508
|
+
|
509
|
+
final_main_app_module_ast = ast.Module(
|
510
|
+
body=final_main_module_imports + other_top_level_nodes_for_main + [main_app_class_node], type_ignores=[]
|
511
|
+
)
|
512
|
+
if hasattr(ast, "fix_missing_locations"):
|
395
513
|
final_main_app_module_ast = ast.fix_missing_locations(final_main_app_module_ast)
|
396
514
|
|
397
515
|
(output_dir / "app.py").write_text(ast.unparse(final_main_app_module_ast))
|
398
516
|
|
399
517
|
(output_dir / "__init__.py").touch(exist_ok=True)
|
400
|
-
(segments_dir / "__init__.py").touch(exist_ok=True)
|
518
|
+
(segments_dir / "__init__.py").touch(exist_ok=True)
|