universal-mcp 0.1.7rc2__py3-none-any.whl → 0.1.8__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/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/ahrefs/README.md +76 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/application.py +95 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +1195 -0
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/e2b/app.py +14 -35
- universal_mcp/applications/figma/README.md +74 -0
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +1261 -0
- universal_mcp/applications/firecrawl/app.py +29 -32
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -138
- universal_mcp/applications/google_docs/app.py +47 -52
- universal_mcp/applications/google_drive/app.py +119 -113
- universal_mcp/applications/google_mail/app.py +124 -50
- universal_mcp/applications/google_sheet/app.py +89 -91
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +13 -45
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -23
- universal_mcp/applications/{serp → serpapi}/app.py +14 -33
- universal_mcp/applications/tavily/app.py +11 -28
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1372 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1428 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +143 -30
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +201 -146
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
- universal_mcp-0.1.8.dist-info/RECORD +81 -0
- universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
- /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
universal_mcp/utils/docgen.py
CHANGED
@@ -5,7 +5,12 @@ using LLMs with structured output
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import ast
|
8
|
+
import json
|
8
9
|
import os
|
10
|
+
import re
|
11
|
+
import sys
|
12
|
+
import textwrap
|
13
|
+
import traceback
|
9
14
|
|
10
15
|
import litellm
|
11
16
|
from pydantic import BaseModel, Field
|
@@ -21,6 +26,14 @@ class DocstringOutput(BaseModel):
|
|
21
26
|
description="Dictionary mapping parameter names to their descriptions"
|
22
27
|
)
|
23
28
|
returns: str = Field(description="Description of what the function returns")
|
29
|
+
raises: dict[str, str] = Field(
|
30
|
+
default_factory=dict,
|
31
|
+
description="Dictionary mapping potential exception types/reasons to their descriptions",
|
32
|
+
)
|
33
|
+
tags: list[str] = Field(
|
34
|
+
default_factory=list,
|
35
|
+
description="List of relevant tags for the function (e.g., action, job type, async status, importance)",
|
36
|
+
)
|
24
37
|
|
25
38
|
|
26
39
|
class FunctionExtractor(ast.NodeVisitor):
|
@@ -47,17 +60,23 @@ class FunctionExtractor(ast.NodeVisitor):
|
|
47
60
|
return None
|
48
61
|
|
49
62
|
def visit_FunctionDef(self, node: ast.FunctionDef):
|
50
|
-
"""Visits a regular function definition."""
|
51
|
-
|
52
|
-
if
|
53
|
-
self.
|
63
|
+
"""Visits a regular function definition and collects it if not excluded."""
|
64
|
+
# Add the exclusion logic here
|
65
|
+
if not node.name.startswith("_") and node.name != "list_tools":
|
66
|
+
source_code = self._get_source_segment(node)
|
67
|
+
if source_code:
|
68
|
+
self.functions.append((node.name, source_code))
|
69
|
+
# Continue traversing the AST for nested functions/classes
|
54
70
|
self.generic_visit(node)
|
55
71
|
|
56
72
|
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
57
|
-
"""Visits an asynchronous function definition."""
|
58
|
-
|
59
|
-
if
|
60
|
-
self.
|
73
|
+
"""Visits an asynchronous function definition and collects it if not excluded."""
|
74
|
+
# Add the exclusion logic here
|
75
|
+
if not node.name.startswith("_") and node.name != "list_tools":
|
76
|
+
source_code = self._get_source_segment(node)
|
77
|
+
if source_code:
|
78
|
+
self.functions.append((node.name, source_code))
|
79
|
+
# Continue traversing the AST for nested functions/classes
|
61
80
|
self.generic_visit(node)
|
62
81
|
|
63
82
|
|
@@ -115,8 +134,49 @@ def extract_functions_from_script(file_path: str) -> list[tuple[str, str]]:
|
|
115
134
|
return []
|
116
135
|
|
117
136
|
|
137
|
+
def extract_json_from_text(text):
|
138
|
+
"""Extract valid JSON from text that might contain additional content.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
text: Raw text response from the model
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
Dict containing the extracted JSON data
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
ValueError: If no valid JSON could be extracted
|
148
|
+
"""
|
149
|
+
# Try to find JSON between triple backticks (common markdown pattern)
|
150
|
+
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text)
|
151
|
+
if json_match:
|
152
|
+
try:
|
153
|
+
return json.loads(json_match.group(1))
|
154
|
+
except json.JSONDecodeError:
|
155
|
+
pass
|
156
|
+
|
157
|
+
# Try to find the first { and last } for a complete JSON object
|
158
|
+
try:
|
159
|
+
start = text.find("{")
|
160
|
+
if start >= 0:
|
161
|
+
brace_count = 0
|
162
|
+
for i in range(start, len(text)):
|
163
|
+
if text[i] == "{":
|
164
|
+
brace_count += 1
|
165
|
+
elif text[i] == "}":
|
166
|
+
brace_count -= 1
|
167
|
+
if brace_count == 0:
|
168
|
+
return json.loads(text[start : i + 1])
|
169
|
+
except json.JSONDecodeError:
|
170
|
+
pass
|
171
|
+
|
172
|
+
try:
|
173
|
+
return json.loads(text)
|
174
|
+
except json.JSONDecodeError as e:
|
175
|
+
raise ValueError("Could not extract valid JSON from the response") from e
|
176
|
+
|
177
|
+
|
118
178
|
def generate_docstring(
|
119
|
-
function_code: str, model: str = "
|
179
|
+
function_code: str, model: str = "perplexity/sonar-pro"
|
120
180
|
) -> DocstringOutput:
|
121
181
|
"""
|
122
182
|
Generate a docstring for a Python function using litellm with structured output.
|
@@ -129,31 +189,42 @@ def generate_docstring(
|
|
129
189
|
A DocstringOutput object containing the structured docstring components
|
130
190
|
"""
|
131
191
|
system_prompt = """You are a helpful AI assistant specialized in writing high-quality Google-style Python docstrings.
|
132
|
-
You MUST ALWAYS include an Args section, even if there are no arguments (in which case mention 'None').
|
192
|
+
You MUST ALWAYS include an Args section, even if there are no arguments (in which case mention 'None').
|
193
|
+
You should also generate a list of tags describing the function's purpose and characteristics."""
|
194
|
+
|
195
|
+
user_prompt = f"""Generate a high-quality Google-style docstring for the following Python function.
|
196
|
+
Analyze the function's name, parameters, return values, potential exceptions, and functionality to create a comprehensive docstring.
|
133
197
|
|
134
|
-
user_prompt = f"""Generate a high-quality Google-style docstring for the following Python function.
|
135
|
-
Analyze the function's name, parameters, return values, and functionality to create a comprehensive docstring.
|
136
|
-
|
137
198
|
The docstring MUST:
|
138
199
|
1. Start with a clear, concise summary of what the function does
|
139
200
|
2. ALWAYS include Args section with description of each parameter (or 'None' if no parameters)
|
140
|
-
3. Include Returns section describing the return value
|
141
|
-
4.
|
142
|
-
|
201
|
+
3. Include Returns section describing the return value (or 'None' if nothing is explicitly returned)
|
202
|
+
4. **Optionally include a Raises section if the function might raise exceptions, describing the exception type/reason and when it's raised.**
|
203
|
+
5. **Include a Tags section with a list of strings describing the function's purpose, characteristics, or keywords.** Tags should be lowercase and single words or hyphenated phrases. Include tags like:
|
204
|
+
- The main action (e.g., 'scrape', 'search', 'start', 'check', 'cancel', 'list')
|
205
|
+
- The type of job ('async_job', 'batch')
|
206
|
+
- The stage of an asynchronous job ('start', 'status', 'cancel')
|
207
|
+
- Related domain/feature ('ai', 'management')
|
208
|
+
- **Significance: Add the tag 'important' to functions that represent core capabilities or primary interaction points of the class (e.g., initiating actions like scrape, search, or starting async jobs).**
|
209
|
+
6. Be formatted according to Google Python Style Guide
|
210
|
+
|
143
211
|
Here is the function:
|
144
|
-
|
212
|
+
|
145
213
|
{function_code}
|
146
|
-
|
147
|
-
Respond in JSON format with the following structure
|
214
|
+
|
215
|
+
Respond ONLY in JSON format with the following structure. **Include the 'raises' field only if the function is likely to raise exceptions.** **Include the 'tags' field as a list of strings.**
|
148
216
|
{{
|
149
|
-
|
150
|
-
|
151
|
-
|
217
|
+
"summary": "A clear, concise summary of what the function does",
|
218
|
+
"args": {{"param_name": "param description", "param_name2": "param description"}},
|
219
|
+
"returns": "Description of what the function returns",
|
220
|
+
"raises": {{
|
221
|
+
"ExceptionType": "Description of when/why this exception is raised"
|
222
|
+
}},
|
223
|
+
"tags": ["tag1", "tag2", "important"]
|
152
224
|
}}
|
153
225
|
"""
|
154
226
|
|
155
227
|
try:
|
156
|
-
# Use regular completion and parse the JSON ourselves instead of using response_model
|
157
228
|
response = litellm.completion(
|
158
229
|
model=model,
|
159
230
|
messages=[
|
@@ -162,149 +233,283 @@ def generate_docstring(
|
|
162
233
|
],
|
163
234
|
)
|
164
235
|
|
165
|
-
# Get the response content
|
166
236
|
response_text = response.choices[0].message.content
|
167
237
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
if not
|
238
|
+
try:
|
239
|
+
parsed_data = extract_json_from_text(response_text)
|
240
|
+
except ValueError as e:
|
241
|
+
print(f"JSON extraction failed: {e}")
|
242
|
+
print(
|
243
|
+
f"Raw response: {response_text[:100]}..."
|
244
|
+
) # Log first 100 chars for debugging
|
245
|
+
# Return a default structure if extraction fails
|
246
|
+
return DocstringOutput(
|
247
|
+
summary="Failed to extract docstring information",
|
248
|
+
args={"None": "This function takes no arguments"},
|
249
|
+
returns="Unknown return value",
|
250
|
+
)
|
251
|
+
model_args = parsed_data.get("args")
|
252
|
+
if not model_args:
|
183
253
|
parsed_data["args"] = {"None": "This function takes no arguments"}
|
184
254
|
|
185
|
-
# Create DocstringOutput from parsed data
|
186
255
|
return DocstringOutput(
|
187
|
-
summary=parsed_data.get("summary", ""),
|
256
|
+
summary=parsed_data.get("summary", "No documentation available"),
|
188
257
|
args=parsed_data.get("args", {"None": "This function takes no arguments"}),
|
189
|
-
returns=parsed_data.get("returns", ""),
|
258
|
+
returns=parsed_data.get("returns", "None"),
|
259
|
+
raises=parsed_data.get("raises", {}),
|
260
|
+
tags=parsed_data.get("tags", []), # Get tags, default to empty list
|
190
261
|
)
|
191
262
|
|
192
263
|
except Exception as e:
|
193
|
-
print(f"Error generating docstring: {e}")
|
194
|
-
|
264
|
+
print(f"Error generating docstring: {e}", file=sys.stderr)
|
265
|
+
traceback.print_exc(file=sys.stderr)
|
195
266
|
return DocstringOutput(
|
196
|
-
summary="
|
267
|
+
summary=f"Error generating docstring: {e}",
|
197
268
|
args={"None": "This function takes no arguments"},
|
198
269
|
returns="None",
|
270
|
+
raises={},
|
271
|
+
tags=["generation-error"],
|
199
272
|
)
|
200
273
|
|
201
274
|
|
202
275
|
def format_docstring(docstring: DocstringOutput) -> str:
|
203
276
|
"""
|
204
|
-
Format a DocstringOutput object into
|
277
|
+
Format a DocstringOutput object into the content string for a docstring.
|
278
|
+
This function produces the content *between* the triple quotes, without
|
279
|
+
the leading/trailing triple quotes or the main indentation.
|
205
280
|
|
206
281
|
Args:
|
207
282
|
docstring: The DocstringOutput object to format
|
208
283
|
|
209
284
|
Returns:
|
210
|
-
A formatted docstring string ready to be
|
285
|
+
A formatted docstring content string ready to be indented and wrapped
|
286
|
+
in triple quotes for insertion into code.
|
211
287
|
"""
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
288
|
+
parts = []
|
289
|
+
|
290
|
+
summary = docstring.summary.strip()
|
291
|
+
if summary:
|
292
|
+
parts.append(summary)
|
293
|
+
|
294
|
+
filtered_args = {
|
295
|
+
name: desc
|
296
|
+
for name, desc in docstring.args.items()
|
297
|
+
if name not in ("self", "cls")
|
298
|
+
}
|
299
|
+
args_lines = []
|
300
|
+
if filtered_args:
|
301
|
+
args_lines.append("Args:")
|
302
|
+
for arg_name, arg_desc in filtered_args.items():
|
303
|
+
arg_desc_cleaned = arg_desc.strip()
|
304
|
+
args_lines.append(f" {arg_name}: {arg_desc_cleaned}")
|
305
|
+
elif docstring.args.get(
|
306
|
+
"None"
|
307
|
+
): # Include the 'None' placeholder if it was generated
|
308
|
+
args_lines.append("Args:")
|
309
|
+
none_desc_cleaned = docstring.args["None"].strip()
|
310
|
+
args_lines.append(f" None: {none_desc_cleaned}")
|
311
|
+
|
312
|
+
if args_lines:
|
313
|
+
parts.append("\n".join(args_lines))
|
314
|
+
|
315
|
+
returns_desc_cleaned = docstring.returns.strip()
|
316
|
+
if returns_desc_cleaned and returns_desc_cleaned.lower() not in ("none", ""):
|
317
|
+
parts.append(f"Returns:\n {returns_desc_cleaned}")
|
318
|
+
|
319
|
+
raises_lines = []
|
320
|
+
if docstring.raises:
|
321
|
+
raises_lines.append("Raises:")
|
322
|
+
for exception_type, exception_desc in docstring.raises.items():
|
323
|
+
exception_desc_cleaned = exception_desc.strip()
|
324
|
+
if (
|
325
|
+
exception_type.strip() and exception_desc_cleaned
|
326
|
+
): # Ensure type and desc are not empty
|
327
|
+
raises_lines.append(
|
328
|
+
f" {exception_type.strip()}: {exception_desc_cleaned}"
|
329
|
+
)
|
330
|
+
if raises_lines:
|
331
|
+
parts.append("\n".join(raises_lines))
|
332
|
+
|
333
|
+
cleaned_tags = [tag.strip() for tag in docstring.tags if tag and tag.strip()]
|
334
|
+
if cleaned_tags:
|
335
|
+
tags_string = ", ".join(cleaned_tags)
|
336
|
+
parts.append(f"Tags:\n {tags_string}")
|
337
|
+
|
338
|
+
return "\n\n".join(parts)
|
224
339
|
|
225
340
|
|
226
341
|
def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
227
342
|
"""
|
228
|
-
Insert a docstring into a function's code
|
343
|
+
Insert a docstring into a function's code, replacing an existing one if present
|
344
|
+
at the correct location, and attempting to remove misplaced string literals
|
345
|
+
from the body.
|
346
|
+
|
347
|
+
This version handles multiline function definitions and existing docstrings
|
348
|
+
by carefully splicing lines based on AST node positions. It also tries to
|
349
|
+
clean up old, misplaced string literals that might have been interpreted
|
350
|
+
as docstrings previously.
|
229
351
|
|
230
352
|
Args:
|
231
|
-
function_code: The source code of the function
|
232
|
-
|
353
|
+
function_code: The source code of the function snippet. This snippet is
|
354
|
+
expected to contain exactly one function definition.
|
355
|
+
docstring: The formatted docstring string content (without triple quotes or
|
356
|
+
leading/trailing newlines within the content itself).
|
233
357
|
|
234
358
|
Returns:
|
235
|
-
The updated function code with the docstring inserted
|
359
|
+
The updated function code with the docstring inserted, or the original
|
360
|
+
code if an error occurs during processing or parsing.
|
236
361
|
"""
|
237
362
|
try:
|
238
|
-
|
239
|
-
if not function_ast.body or not hasattr(function_ast.body[0], "body"):
|
240
|
-
return function_code
|
363
|
+
lines = function_code.splitlines(keepends=True)
|
241
364
|
|
242
|
-
|
365
|
+
tree = ast.parse(function_code)
|
366
|
+
if not tree.body or not isinstance(
|
367
|
+
tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef
|
368
|
+
):
|
369
|
+
print(
|
370
|
+
"Warning: Could not parse function definition from code snippet. Returning original code.",
|
371
|
+
file=sys.stderr,
|
372
|
+
)
|
373
|
+
return function_code # Return original code if parsing fails or isn't a function
|
243
374
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
func_def_line = i
|
249
|
-
break
|
375
|
+
func_node = tree.body[0]
|
376
|
+
func_name = getattr(func_node, "name", "unknown_function")
|
377
|
+
|
378
|
+
insert_idx = func_node.end_lineno
|
250
379
|
|
251
|
-
if
|
252
|
-
|
380
|
+
if func_node.body:
|
381
|
+
insert_idx = func_node.body[0].lineno - 1
|
253
382
|
|
254
|
-
#
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
383
|
+
body_indent = " " # Default indentation (PEP 8)
|
384
|
+
|
385
|
+
indent_source_idx = insert_idx
|
386
|
+
actual_first_body_line_idx = -1
|
387
|
+
for i in range(indent_source_idx, len(lines)):
|
388
|
+
line = lines[i]
|
389
|
+
stripped = line.lstrip()
|
390
|
+
if stripped and not stripped.startswith("#"):
|
391
|
+
actual_first_body_line_idx = i
|
259
392
|
break
|
260
393
|
|
261
|
-
#
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
394
|
+
# If a meaningful line was found at or after insertion point, use its indentation
|
395
|
+
if actual_first_body_line_idx != -1:
|
396
|
+
body_line = lines[actual_first_body_line_idx]
|
397
|
+
body_indent = body_line[: len(body_line) - len(body_line.lstrip())]
|
398
|
+
else:
|
399
|
+
if func_node.lineno - 1 < len(lines): # Ensure def line exists
|
400
|
+
def_line = lines[func_node.lineno - 1]
|
401
|
+
def_line_indent = def_line[: len(def_line) - len(def_line.lstrip())]
|
402
|
+
body_indent = (
|
403
|
+
def_line_indent + " "
|
404
|
+
) # Standard 4 spaces relative indent
|
405
|
+
|
406
|
+
# Format the new docstring lines with the calculated indentation
|
407
|
+
new_docstring_lines_formatted = [f'{body_indent}"""\n']
|
408
|
+
new_docstring_lines_formatted.extend(
|
409
|
+
[f"{body_indent}{line}\n" for line in docstring.splitlines()]
|
269
410
|
)
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
411
|
+
new_docstring_lines_formatted.append(f'{body_indent}"""\n')
|
412
|
+
|
413
|
+
output_lines = []
|
414
|
+
output_lines.extend(lines[:insert_idx])
|
415
|
+
|
416
|
+
# 2. Insert the new docstring
|
417
|
+
output_lines.extend(new_docstring_lines_formatted)
|
418
|
+
remaining_body_lines = lines[insert_idx:]
|
419
|
+
|
420
|
+
remaining_body_code = "".join(remaining_body_lines)
|
421
|
+
|
422
|
+
if remaining_body_code.strip(): # Only parse if there's non-whitespace content
|
423
|
+
try:
|
424
|
+
dummy_code = f"def _dummy_func():\n{textwrap.indent(remaining_body_code, body_indent)}"
|
425
|
+
dummy_tree = ast.parse(dummy_code)
|
426
|
+
dummy_body_statements = (
|
427
|
+
dummy_tree.body[0].body
|
428
|
+
if dummy_tree.body
|
429
|
+
and isinstance(
|
430
|
+
dummy_tree.body[0], ast.FunctionDef | ast.AsyncFunctionDef
|
431
|
+
)
|
432
|
+
else []
|
433
|
+
)
|
434
|
+
cleaned_body_parts = []
|
435
|
+
for _node in dummy_body_statements:
|
436
|
+
break # Exit this loop, we'll process func_node.body instead
|
437
|
+
cleaned_body_parts = []
|
438
|
+
start_stmt_index = (
|
439
|
+
1
|
440
|
+
if func_node.body
|
441
|
+
and isinstance(func_node.body[0], ast.Expr)
|
442
|
+
and isinstance(func_node.body[0].value, ast.Constant)
|
443
|
+
and isinstance(func_node.body[0].value.value, str)
|
444
|
+
else 0
|
445
|
+
)
|
446
|
+
|
447
|
+
for i in range(start_stmt_index, len(func_node.body)):
|
448
|
+
stmt_node = func_node.body[i]
|
449
|
+
|
450
|
+
is_just_string_stmt = (
|
451
|
+
isinstance(stmt_node, ast.Expr)
|
452
|
+
and isinstance(stmt_node.value, ast.Constant)
|
453
|
+
and isinstance(stmt_node.value.value, str)
|
454
|
+
)
|
455
|
+
|
456
|
+
if not is_just_string_stmt:
|
457
|
+
stmt_start_idx = stmt_node.lineno - 1
|
458
|
+
stmt_end_idx = (
|
459
|
+
stmt_node.end_lineno - 1
|
460
|
+
) # Inclusive end line index
|
461
|
+
|
462
|
+
cleaned_body_parts.extend(
|
463
|
+
lines[stmt_start_idx : stmt_end_idx + 1]
|
464
|
+
)
|
465
|
+
|
466
|
+
if func_node.body:
|
467
|
+
last_stmt_end_idx = func_node.body[-1].end_lineno - 1
|
468
|
+
for line in lines[last_stmt_end_idx + 1 :]:
|
469
|
+
if line.strip():
|
470
|
+
cleaned_body_parts.append(line)
|
471
|
+
cleaned_body_lines = cleaned_body_parts
|
472
|
+
|
473
|
+
except SyntaxError as parse_e:
|
474
|
+
print(
|
475
|
+
f"WARNING: Could not parse function body for cleaning, keeping all body lines: {parse_e}",
|
476
|
+
file=sys.stderr,
|
477
|
+
)
|
478
|
+
traceback.print_exc(file=sys.stderr)
|
479
|
+
cleaned_body_lines = remaining_body_lines
|
480
|
+
except Exception as other_e:
|
481
|
+
print(
|
482
|
+
f"WARNING: Unexpected error processing function body for cleaning, keeping all body lines: {other_e}",
|
483
|
+
file=sys.stderr,
|
484
|
+
)
|
485
|
+
traceback.print_exc(file=sys.stderr)
|
486
|
+
cleaned_body_lines = remaining_body_lines
|
292
487
|
else:
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
)
|
488
|
+
cleaned_body_lines = []
|
489
|
+
output_lines.extend(lines[func_node.end_lineno :])
|
490
|
+
|
491
|
+
if func_node.body or not remaining_body_code.strip():
|
492
|
+
output_lines.extend(cleaned_body_lines)
|
299
493
|
|
300
|
-
|
494
|
+
final_code = "".join(output_lines)
|
495
|
+
ast.parse(final_code)
|
496
|
+
return final_code
|
497
|
+
|
498
|
+
except SyntaxError as e:
|
499
|
+
print(
|
500
|
+
f"WARNING: Generated code snippet for '{func_name}' has syntax error: {e}",
|
501
|
+
file=sys.stderr,
|
502
|
+
)
|
503
|
+
traceback.print_exc(file=sys.stderr)
|
301
504
|
return function_code
|
302
505
|
except Exception as e:
|
303
|
-
print(f"Error
|
506
|
+
print(f"Error processing function snippet for insertion: {e}", file=sys.stderr)
|
507
|
+
traceback.print_exc(file=sys.stderr)
|
508
|
+
|
304
509
|
return function_code
|
305
510
|
|
306
511
|
|
307
|
-
def process_file(file_path: str, model: str = "
|
512
|
+
def process_file(file_path: str, model: str = "perplexity/sonar-pro") -> int:
|
308
513
|
"""
|
309
514
|
Process a Python file and add docstrings to all functions in it.
|
310
515
|
|
@@ -355,6 +560,7 @@ def process_file(file_path: str, model: str = "openai/gpt-4o") -> int:
|
|
355
560
|
f.write(updated_content)
|
356
561
|
print(f"Updated {count} functions in {file_path}")
|
357
562
|
else:
|
563
|
+
print(updated_function, "formatted docstring", formatted_docstring)
|
358
564
|
print(f"No changes made to {file_path}")
|
359
565
|
|
360
566
|
return count
|