universal-mcp 0.1.7rc1__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.
Files changed (61) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/ahrefs/README.md +76 -0
  4. universal_mcp/applications/ahrefs/app.py +2291 -0
  5. universal_mcp/applications/application.py +95 -5
  6. universal_mcp/applications/calendly/README.md +78 -0
  7. universal_mcp/applications/calendly/__init__.py +0 -0
  8. universal_mcp/applications/calendly/app.py +1195 -0
  9. universal_mcp/applications/coda/README.md +133 -0
  10. universal_mcp/applications/coda/__init__.py +0 -0
  11. universal_mcp/applications/coda/app.py +3671 -0
  12. universal_mcp/applications/e2b/app.py +14 -28
  13. universal_mcp/applications/figma/README.md +74 -0
  14. universal_mcp/applications/figma/__init__.py +0 -0
  15. universal_mcp/applications/figma/app.py +1261 -0
  16. universal_mcp/applications/firecrawl/app.py +38 -35
  17. universal_mcp/applications/github/app.py +127 -85
  18. universal_mcp/applications/google_calendar/app.py +62 -138
  19. universal_mcp/applications/google_docs/app.py +47 -52
  20. universal_mcp/applications/google_drive/app.py +119 -113
  21. universal_mcp/applications/google_mail/app.py +124 -50
  22. universal_mcp/applications/google_sheet/app.py +89 -91
  23. universal_mcp/applications/markitdown/app.py +9 -8
  24. universal_mcp/applications/notion/app.py +254 -134
  25. universal_mcp/applications/perplexity/app.py +13 -41
  26. universal_mcp/applications/reddit/app.py +94 -85
  27. universal_mcp/applications/resend/app.py +12 -13
  28. universal_mcp/applications/{serp → serpapi}/app.py +14 -25
  29. universal_mcp/applications/tavily/app.py +11 -18
  30. universal_mcp/applications/wrike/README.md +71 -0
  31. universal_mcp/applications/wrike/__init__.py +0 -0
  32. universal_mcp/applications/wrike/app.py +1372 -0
  33. universal_mcp/applications/youtube/README.md +82 -0
  34. universal_mcp/applications/youtube/__init__.py +0 -0
  35. universal_mcp/applications/youtube/app.py +1428 -0
  36. universal_mcp/applications/zenquotes/app.py +12 -2
  37. universal_mcp/exceptions.py +9 -2
  38. universal_mcp/integrations/__init__.py +24 -1
  39. universal_mcp/integrations/agentr.py +27 -4
  40. universal_mcp/integrations/integration.py +146 -32
  41. universal_mcp/logger.py +3 -56
  42. universal_mcp/servers/__init__.py +6 -14
  43. universal_mcp/servers/server.py +201 -146
  44. universal_mcp/stores/__init__.py +7 -2
  45. universal_mcp/stores/store.py +103 -40
  46. universal_mcp/tools/__init__.py +3 -0
  47. universal_mcp/tools/adapters.py +43 -0
  48. universal_mcp/tools/func_metadata.py +213 -0
  49. universal_mcp/tools/tools.py +342 -0
  50. universal_mcp/utils/docgen.py +325 -119
  51. universal_mcp/utils/docstring_parser.py +179 -0
  52. universal_mcp/utils/dump_app_tools.py +33 -23
  53. universal_mcp/utils/installation.py +201 -10
  54. universal_mcp/utils/openapi.py +229 -46
  55. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
  56. universal_mcp-0.1.8.dist-info/RECORD +81 -0
  57. universal_mcp-0.1.7rc1.dist-info/RECORD +0 -58
  58. /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
  59. /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
  60. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
  61. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -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
- source_code = self._get_source_segment(node)
52
- if source_code:
53
- self.functions.append((node.name, source_code))
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
- source_code = self._get_source_segment(node)
59
- if source_code:
60
- self.functions.append((node.name, source_code))
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 = "openai/gpt-4o"
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. Be formatted according to Google Python Style Guide
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
- "summary": "A clear, concise summary of what the function does",
150
- "args": {{"param_name": "param description", "param_name2": "param description"}},
151
- "returns": "Description of what the function returns"
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
- # Simple JSON extraction in case the model includes extra text
169
- import json
170
- import re
171
-
172
- # Find JSON object in the response using regex
173
- json_match = re.search(r"({.*})", response_text.replace("\n", " "), re.DOTALL)
174
- if json_match:
175
- json_str = json_match.group(1)
176
- parsed_data = json.loads(json_str)
177
- else:
178
- # Try to parse the whole response as JSON
179
- parsed_data = json.loads(response_text)
180
-
181
- # Ensure args is never empty
182
- if not parsed_data.get("args"):
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
- # Return a docstring object with default values
264
+ print(f"Error generating docstring: {e}", file=sys.stderr)
265
+ traceback.print_exc(file=sys.stderr)
195
266
  return DocstringOutput(
196
- summary="No documentation available",
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 a properly formatted docstring string.
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 inserted into code
285
+ A formatted docstring content string ready to be indented and wrapped
286
+ in triple quotes for insertion into code.
211
287
  """
212
- formatted_docstring = f"{docstring.summary}\n\n"
213
-
214
- if docstring.args:
215
- formatted_docstring += "Args:\n"
216
- for arg_name, arg_desc in docstring.args.items():
217
- formatted_docstring += f" {arg_name}: {arg_desc}\n"
218
- formatted_docstring += "\n"
219
-
220
- if docstring.returns:
221
- formatted_docstring += f"Returns:\n {docstring.returns}\n"
222
-
223
- return formatted_docstring.strip()
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
- docstring: The formatted docstring string to insert
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
- function_ast = ast.parse(function_code)
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
- function_lines = function_code.splitlines()
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
- # Find the function definition line (ends with ':')
245
- func_def_line = None
246
- for i, line in enumerate(function_lines):
247
- if "def " in line and line.strip().endswith(":"):
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 func_def_line is None:
252
- return function_code
380
+ if func_node.body:
381
+ insert_idx = func_node.body[0].lineno - 1
253
382
 
254
- # Determine indentation from the first non-empty line after the function definition
255
- body_indent = ""
256
- for line in function_lines[func_def_line + 1 :]:
257
- if line.strip():
258
- body_indent = " " * (len(line) - len(line.lstrip()))
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
- # Check if the function already has a docstring
262
- first_element = (
263
- function_ast.body[0].body[0] if function_ast.body[0].body else None
264
- )
265
- has_docstring = (
266
- isinstance(first_element, ast.Expr)
267
- and isinstance(first_element.value, ast.Constant)
268
- and isinstance(first_element.value.value, str)
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
- docstring_lines = [
272
- f'{body_indent}"""',
273
- *[f"{body_indent}{line}" for line in docstring.split("\n")],
274
- f'{body_indent}"""',
275
- ]
276
-
277
- if has_docstring:
278
- # Find the existing docstring in the source and replace it
279
- for i in range(func_def_line + 1, len(function_lines)):
280
- if '"""' in function_lines[i] or "'''" in function_lines[i]:
281
- docstring_start = i
282
- # Find end of docstring
283
- for j in range(docstring_start + 1, len(function_lines)):
284
- if '"""' in function_lines[j] or "'''" in function_lines[j]:
285
- docstring_end = j
286
- # Replace the existing docstring
287
- return "\n".join(
288
- function_lines[:docstring_start]
289
- + docstring_lines
290
- + function_lines[docstring_end + 1 :]
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
- # Insert new docstring after function definition
294
- return "\n".join(
295
- function_lines[: func_def_line + 1]
296
- + docstring_lines
297
- + function_lines[func_def_line + 1 :]
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
- # Default return if insertion logic fails
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 inserting docstring: {e}")
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 = "openai/gpt-4o") -> int:
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