Agents-MCP-Hackathon/Python-Code-to-Diagram-Generator-MCP
6
1import gradio as gr2import sys3import os4import tempfile5import shutil6import ast7import time8import subprocess9import re10from typing import List, Dict, Optional, Tuple, Any11from py2puml.py2puml import py2puml12from plantuml import PlantUML13import pyan14from pathlib import Path15from utils import setup_testing_space, verify_testing_space, cleanup_testing_space16 17if os.name == "nt": # nt == Windows18 graphviz_bin = r"C:\\Program Files\\Graphviz\\bin"19 if graphviz_bin not in os.environ["PATH"]:20 os.environ["PATH"] += os.pathsep + graphviz_bin21 22 23def generate_call_graph_with_pyan3(24 python_code: str, filename: str = "analysis"25) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:26 """Generate call graph using pyan3 and return DOT content, PNG path, and structured data.27 28 Args:29 python_code: The Python code to analyze30 filename: Base filename for temporary files31 32 Returns:33 Tuple of (dot_content, png_path, structured_data)34 """35 if not python_code.strip():36 return None, None, {}37 38 # Create unique filename using timestamp39 timestamp = str(int(time.time() * 1000))40 unique_filename = f"{filename}_{timestamp}"41 42 # Paths43 testing_dir = os.path.join(os.getcwd(), "inputs")44 code_file = os.path.join(testing_dir, f"{unique_filename}.py")45 46 try:47 # Write Python code to file48 with open(code_file, "w", encoding="utf-8") as f:49 f.write(python_code)50 51 print(f"📊 Generating call graph for: {unique_filename}.py")52 53 try:54 55 dot_content = pyan.create_callgraph(56 filenames=[str(code_file)],57 format="dot",58 colored=True,59 grouped=True,60 annotated=True,61 )62 63 png_path = None64 with tempfile.TemporaryDirectory() as temp_dir:65 dot_file = os.path.join(temp_dir, f"{unique_filename}.dot")66 temp_png = os.path.join(temp_dir, f"{unique_filename}.png")67 68 # Write DOT content to file69 with open(dot_file, "w", encoding="utf-8") as f:70 f.write(dot_content)71 72 # Generate PNG using dot command73 dot_cmd = ["dot", "-Tpng", dot_file, "-o", temp_png]74 75 try:76 subprocess.run(dot_cmd, check=True, timeout=30)77 78 if os.path.exists(temp_png):79 # Copy to permanent location80 permanent_dir = os.path.join(os.getcwd(), "temp_diagrams")81 os.makedirs(permanent_dir, exist_ok=True)82 png_path = os.path.join(83 permanent_dir, f"callgraph_{unique_filename}.png"84 )85 shutil.copy2(temp_png, png_path)86 print(f"🎨 Call graph PNG saved: {os.path.basename(png_path)}")87 88 except subprocess.SubprocessError as e:89 print(f"⚠️ Graphviz PNG generation failed: {e}")90 # Continue without PNG, DOT content is still useful91 92 # Parse DOT content for structured data93 structured_data = parse_call_graph_data(dot_content)94 95 return dot_content, png_path, structured_data96 97 except subprocess.TimeoutExpired:98 print("⚠️ pyan3 analysis timed out, trying simplified approach...")99 return try_fallback_analysis(python_code, unique_filename)100 except subprocess.SubprocessError as e:101 print(f"⚠️ pyan3 execution failed: {e}, trying fallback...")102 return try_fallback_analysis(python_code, unique_filename)103 104 except Exception as e:105 print(f"❌ Call graph generation error: {e}")106 return None, None, {"error": str(e)}107 108 finally:109 # Clean up temporary file110 if os.path.exists(code_file):111 try:112 os.remove(code_file)113 print(f"🧹 Cleaned up analysis file: {unique_filename}.py")114 except Exception as e:115 print(f"⚠️ Could not remove analysis file: {e}")116 117 118def parse_call_graph_data(dot_content: str) -> Dict[str, Any]:119 """Parse pyan3 DOT output into structured function call data.120 121 Args:122 dot_content: DOT format string from pyan3123 124 Returns:125 Dictionary with parsed call graph information126 """127 if not dot_content:128 return {}129 130 try:131 # Extract nodes (functions/classes)132 node_pattern = r'"([^"]+)"\s*\['133 nodes = re.findall(node_pattern, dot_content)134 135 # Extract edges (function calls)136 edge_pattern = r'"([^"]+)"\s*->\s*"([^"]+)"'137 edges = re.findall(edge_pattern, dot_content)138 139 # Build function call mapping140 call_graph = {}141 called_by = {}142 143 for caller, callee in edges:144 if caller not in call_graph:145 call_graph[caller] = []146 call_graph[caller].append(callee)147 148 if callee not in called_by:149 called_by[callee] = []150 called_by[callee].append(caller)151 152 # Calculate metrics153 function_metrics = {}154 for node in nodes:155 out_degree = len(call_graph.get(node, []))156 in_degree = len(called_by.get(node, []))157 158 function_metrics[node] = {159 "calls_made": out_degree,160 "called_by_count": in_degree,161 "calls_to": call_graph.get(node, []),162 "called_by": called_by.get(node, []),163 }164 165 return {166 "nodes": nodes,167 "edges": edges,168 "total_functions": len(nodes),169 "total_calls": len(edges),170 "call_graph": call_graph,171 "function_metrics": function_metrics,172 }173 174 except Exception as e:175 return {"parse_error": str(e)}176 177 178def try_fallback_analysis(179 python_code: str, unique_filename: str180) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:181 """Fallback analysis when pyan3 fails - basic function call detection.182 183 Args:184 python_code: The Python code to analyze185 unique_filename: Unique filename for this analysis186 187 Returns:188 Tuple of (None, None, fallback_analysis_data)189 """190 print("🔄 Using fallback analysis approach...")191 192 try:193 import ast194 import re195 196 tree = ast.parse(python_code)197 functions = []198 calls = []199 200 # Extract function definitions201 for node in ast.walk(tree):202 if isinstance(node, ast.FunctionDef):203 functions.append(node.name)204 205 # Simple regex-based call detection (fallback approach)206 for func in functions:207 # Look for calls to this function208 pattern = rf"\b{re.escape(func)}\s*\("209 if re.search(pattern, python_code):210 calls.append(("unknown", func))211 212 return (213 None,214 None,215 {216 "fallback": True,217 "functions_detected": functions,218 "total_functions": len(functions),219 "total_calls": len(calls),220 "info": f"Fallback analysis: detected {len(functions)} functions",221 "function_metrics": {222 func: {223 "calls_made": 0,224 "called_by_count": 0,225 "calls_to": [],226 "called_by": [],227 }228 for func in functions229 },230 },231 )232 233 except Exception as e:234 return None, None, {"error": f"Fallback analysis also failed: {str(e)}"}235 236 237def analyze_function_complexity(python_code: str) -> Dict[str, Any]:238 """Analyze function complexity using AST.239 240 Args:241 python_code: The Python code to analyze242 243 Returns:244 Dictionary with function complexity metrics245 """246 if not python_code.strip():247 return {}248 249 try:250 tree = ast.parse(python_code)251 function_analysis = {}252 253 for node in ast.walk(tree):254 if isinstance(node, ast.FunctionDef):255 # Calculate cyclomatic complexity (simplified)256 complexity = 1 # Base complexity257 258 for child in ast.walk(node):259 if isinstance(260 child,261 (262 ast.If,263 ast.While,264 ast.For,265 ast.Try,266 ast.ExceptHandler,267 ast.With,268 ast.Assert,269 ),270 ):271 complexity += 1272 elif isinstance(child, ast.BoolOp):273 complexity += len(child.values) - 1274 275 # Count lines of code276 lines = (277 node.end_lineno - node.lineno + 1278 if hasattr(node, "end_lineno")279 else 0280 )281 282 # Extract parameters283 params = [arg.arg for arg in node.args.args]284 285 # Check for docstring286 has_docstring = (287 len(node.body) > 0288 and isinstance(node.body[0], ast.Expr)289 and isinstance(node.body[0].value, ast.Constant)290 and isinstance(node.body[0].value.value, str)291 )292 293 function_analysis[node.name] = {294 "complexity": complexity,295 "lines_of_code": lines,296 "parameter_count": len(params),297 "parameters": params,298 "has_docstring": has_docstring,299 "line_start": node.lineno,300 "line_end": getattr(node, "end_lineno", node.lineno),301 }302 303 return function_analysis304 305 except Exception as e:306 return {"error": str(e)}307 308 309def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[str]:310 """Generate a UML class diagram from Python code.311 312 Args:313 python_code: The Python code to analyze and convert to UML314 filename: Optional name for the generated diagram file315 316 Returns:317 Path to the generated PNG diagram image or None if failed318 """319 if not python_code.strip():320 return None321 322 print(f"🔄 Processing code for diagram generation...")323 324 # Clean testing space (ensure only __init__.py exists)325 cleanup_testing_space()326 327 # Verify clean state328 if not verify_testing_space():329 print("⚠️ testing_space verification failed, recreating...")330 setup_testing_space()331 cleanup_testing_space()332 333 # Create unique filename using timestamp334 timestamp = str(int(time.time() * 1000)) # millisecond timestamp335 unique_filename = f"{filename}_{timestamp}"336 337 # Paths338 testing_dir = os.path.join(os.getcwd(), "inputs")339 code_file = os.path.join(testing_dir, f"{unique_filename}.py")340 341 # Use PlantUML web service for rendering342 server = PlantUML(url="http://www.plantuml.com/plantuml/img/")343 344 try:345 # Write Python code to file in testing_space346 with open(code_file, "w", encoding="utf-8") as f:347 f.write(python_code)348 349 print(f"📝 Created temporary file: inputs/{unique_filename}.py")350 351 # Generate PlantUML content using py2puml (no sys.path manipulation needed)352 print(f"📝 Generating PlantUML content...")353 puml_content_lines = py2puml(354 os.path.join(355 testing_dir, unique_filename356 ), # path to the .py file (without extension)357 f"inputs.{unique_filename}", # module name358 )359 puml_content = "".join(puml_content_lines)360 361 if not puml_content.strip():362 print("⚠️ No UML content generated - check if your code contains classes")363 return None364 365 # Create temporary directory for PlantUML processing366 with tempfile.TemporaryDirectory() as temp_dir:367 # Save PUML file368 puml_file = os.path.join(temp_dir, f"{unique_filename}.puml")369 with open(puml_file, "w", encoding="utf-8") as f:370 f.write(puml_content)371 372 print(f"🎨 Rendering diagram...")373 # Generate PNG374 output_png = os.path.join(temp_dir, f"{unique_filename}.png")375 server.processes_file(puml_file, outfile=output_png)376 377 if os.path.exists(output_png):378 print("✅ Diagram generated successfully!")379 # Copy to a permanent location for Gradio to serve380 permanent_dir = os.path.join(os.getcwd(), "temp_diagrams")381 os.makedirs(permanent_dir, exist_ok=True)382 permanent_path = os.path.join(383 permanent_dir, f"{filename}_{hash(python_code) % 10000}.png"384 )385 shutil.copy2(output_png, permanent_path)386 return permanent_path387 else:388 print("❌ Failed to generate PNG")389 return None390 391 except Exception as e:392 print(f"❌ Error: {e}")393 return None394 395 finally:396 # Always clean up the temporary .py file397 if os.path.exists(code_file):398 try:399 os.remove(code_file)400 print(f"🧹 Cleaned up temporary file: {unique_filename}.py")401 except Exception as e:402 print(f"⚠️ Could not remove temporary file: {e}")403 404 405def analyze_code_structure(python_code: str) -> str:406 """Return a Markdown report with complexity metrics and recommendations.407 408 Args:409 python_code: The Python code to analyze410 411 Returns:412 Comprehensive analysis report in markdown format413 """414 if not python_code.strip():415 return "No code provided for analysis."416 417 try:418 # Basic AST analysis419 tree = ast.parse(python_code)420 classes = []421 functions = []422 imports = []423 424 for node in ast.walk(tree):425 if isinstance(node, ast.ClassDef):426 methods = []427 attributes = []428 429 for item in node.body:430 if isinstance(item, ast.FunctionDef):431 methods.append(item.name)432 elif isinstance(item, ast.Assign):433 for target in item.targets:434 if isinstance(target, ast.Name):435 attributes.append(target.id)436 437 # Check for inheritance438 parents = [base.id for base in node.bases if isinstance(base, ast.Name)]439 440 classes.append(441 {442 "name": node.name,443 "methods": methods,444 "attributes": attributes,445 "parents": parents,446 }447 )448 449 elif isinstance(node, ast.FunctionDef):450 # Check if it's a top-level function (not inside a class)451 is_method = any(452 isinstance(parent, ast.ClassDef)453 for parent in ast.walk(tree)454 if hasattr(parent, "body") and node in getattr(parent, "body", [])455 )456 if not is_method:457 functions.append(node.name)458 459 elif isinstance(node, (ast.Import, ast.ImportFrom)):460 if isinstance(node, ast.Import):461 for alias in node.names:462 imports.append(alias.name)463 else:464 module = node.module or ""465 for alias in node.names:466 imports.append(467 f"{module}.{alias.name}" if module else alias.name468 )469 470 # Enhanced function complexity analysis471 function_complexity = analyze_function_complexity(python_code)472 473 # Call graph analysis (for files with functions)474 call_graph_data = {}475 if functions or any(classes): # Only run if there are functions to analyze476 try:477 cleanup_testing_space() # Ensure clean state478 dot_content, png_path, call_graph_data = generate_call_graph_with_pyan3(479 python_code480 )481 except Exception as e:482 print(f"⚠️ Call graph analysis failed: {e}")483 call_graph_data = {"error": str(e)}484 485 # Build comprehensive summary486 summary = "📊 **Enhanced Code Analysis Results**\n\n"487 488 # === OVERVIEW SECTION ===489 summary += "## 📋 **Overview**\n"490 summary += f"• **{len(classes)}** classes found\n"491 summary += f"• **{len(functions)}** standalone functions found\n"492 summary += f"• **{len(set(imports))}** unique imports\n"493 494 if call_graph_data and "total_functions" in call_graph_data:495 summary += f"• **{call_graph_data['total_functions']}** total functions/methods in call graph\n"496 summary += (497 f"• **{call_graph_data['total_calls']}** function calls detected\n"498 )499 500 summary += "\n"501 502 # === CLASSES SECTION ===503 if classes:504 summary += "## 🏗️ **Classes**\n"505 for cls in classes:506 summary += f"### **{cls['name']}**\n"507 if cls["parents"]:508 summary += f" - **Inherits from**: {', '.join(cls['parents'])}\n"509 summary += f" - **Methods**: {len(cls['methods'])}"510 if cls["methods"]:511 summary += f" ({', '.join(cls['methods'])})"512 summary += "\n"513 if cls["attributes"]:514 summary += f" - **Attributes**: {', '.join(cls['attributes'])}\n"515 summary += "\n"516 517 # === STANDALONE FUNCTIONS SECTION ===518 if functions:519 summary += "## ⚙️ **Standalone Functions**\n"520 for func in functions:521 summary += f"### **{func}()**\n"522 523 # Add complexity metrics if available524 if func in function_complexity:525 metrics = function_complexity[func]526 summary += (527 f" - **Complexity**: {metrics['complexity']} (cyclomatic)\n"528 )529 summary += f" - **Lines of Code**: {metrics['lines_of_code']}\n"530 summary += f" - **Parameters**: {metrics['parameter_count']}"531 if metrics["parameters"]:532 summary += f" ({', '.join(metrics['parameters'])})"533 summary += "\n"534 summary += f" - **Has Docstring**: {'✅' if metrics['has_docstring'] else '❌'}\n"535 summary += f" - **Lines**: {metrics['line_start']}-{metrics['line_end']}\n"536 537 # Add call graph info if available538 if call_graph_data and "function_metrics" in call_graph_data:539 if func in call_graph_data["function_metrics"]:540 call_metrics = call_graph_data["function_metrics"][func]541 summary += f" - **Calls Made**: {call_metrics['calls_made']}\n"542 if call_metrics["calls_to"]:543 summary += (544 f" - Calls: {', '.join(call_metrics['calls_to'])}\n"545 )546 summary += f" - **Called By**: {call_metrics['called_by_count']} functions\n"547 if call_metrics["called_by"]:548 summary += f" - Called by: {', '.join(call_metrics['called_by'])}\n"549 550 summary += "\n"551 552 # === CALL GRAPH ANALYSIS ===553 if (554 call_graph_data555 and "function_metrics" in call_graph_data556 and call_graph_data["total_calls"] > 0557 ):558 summary += "## 🔗 **Function Call Analysis**\n"559 560 # Most called functions561 sorted_by_calls = sorted(562 call_graph_data["function_metrics"].items(),563 key=lambda x: x[1]["called_by_count"],564 reverse=True,565 )[:5]566 567 if sorted_by_calls and sorted_by_calls[0][1]["called_by_count"] > 0:568 summary += "**Most Called Functions:**\n"569 for func_name, metrics in sorted_by_calls:570 if metrics["called_by_count"] > 0:571 summary += f"• **{func_name}**: called {metrics['called_by_count']} times\n"572 summary += "\n"573 574 # Most complex functions (by calls made)575 sorted_by_complexity = sorted(576 call_graph_data["function_metrics"].items(),577 key=lambda x: x[1]["calls_made"],578 reverse=True,579 )[:5]580 581 if sorted_by_complexity and sorted_by_complexity[0][1]["calls_made"] > 0:582 summary += "**Functions Making Most Calls:**\n"583 for func_name, metrics in sorted_by_complexity:584 if metrics["calls_made"] > 0:585 summary += (586 f"• **{func_name}**: makes {metrics['calls_made']} calls\n"587 )588 summary += "\n"589 590 # === COMPLEXITY ANALYSIS ===591 if function_complexity:592 summary += "## 📈 **Complexity Analysis**\n"593 594 # Sort by complexity595 sorted_complexity = sorted(596 function_complexity.items(),597 key=lambda x: x[1]["complexity"],598 reverse=True,599 )[:5]600 601 summary += "**Most Complex Functions:**\n"602 for func_name, metrics in sorted_complexity:603 summary += f"• **{func_name}**: complexity {metrics['complexity']}, {metrics['lines_of_code']} lines\n"604 605 # Overall stats606 total_functions = len(function_complexity)607 avg_complexity = (608 sum(m["complexity"] for m in function_complexity.values())609 / total_functions610 )611 avg_lines = (612 sum(m["lines_of_code"] for m in function_complexity.values())613 / total_functions614 )615 functions_with_docs = sum(616 1 for m in function_complexity.values() if m["has_docstring"]617 )618 619 summary += "\n**Overall Function Metrics:**\n"620 summary += f"• **Average Complexity**: {avg_complexity:.1f}\n"621 summary += f"• **Average Lines per Function**: {avg_lines:.1f}\n"622 summary += f"• **Functions with Docstrings**: {functions_with_docs}/{total_functions} ({100*functions_with_docs/total_functions:.1f}%)\n"623 summary += "\n"624 625 # === IMPORTS SECTION ===626 if imports:627 summary += "## 📦 **Imports**\n"628 unique_imports = list(set(imports))629 for imp in unique_imports[:10]: # Show first 10 imports630 summary += f"• {imp}\n"631 if len(unique_imports) > 10:632 summary += f"• ... and {len(unique_imports) - 10} more\n"633 summary += "\n"634 635 # === CALL GRAPH ERROR/INFO ===636 if call_graph_data and "error" in call_graph_data:637 summary += "## ⚠️ **Call Graph Analysis**\n"638 summary += f"Call graph generation failed: {call_graph_data['error']}\n\n"639 elif call_graph_data and "info" in call_graph_data:640 summary += "## 📊 **Call Graph Analysis**\n"641 summary += f"{call_graph_data['info']}\n\n"642 643 # === RECOMMENDATIONS ===644 summary += "## 💡 **Recommendations**\n"645 if function_complexity:646 high_complexity = [647 f for f, m in function_complexity.items() if m["complexity"] > 10648 ]649 if high_complexity:650 summary += f"• Consider refactoring high-complexity functions: {', '.join(high_complexity)}\n"651 652 no_docs = [653 f for f, m in function_complexity.items() if not m["has_docstring"]654 ]655 if no_docs:656 summary += f"• Add docstrings to: {', '.join(no_docs[:5])}{'...' if len(no_docs) > 5 else ''}\n"657 658 if call_graph_data and "function_metrics" in call_graph_data:659 isolated_functions = [660 f661 for f, m in call_graph_data["function_metrics"].items()662 if m["calls_made"] == 0 and m["called_by_count"] == 0663 ]664 if isolated_functions:665 summary += f"• Review isolated functions: {', '.join(isolated_functions[:3])}{'...' if len(isolated_functions) > 3 else ''}\n"666 667 return summary668 669 except SyntaxError as e:670 return f"❌ **Syntax Error in Python code:**\n```\n{str(e)}\n```"671 except Exception as e:672 return f"❌ **Error analyzing code:**\n```\n{str(e)}\n```"673 674 675def list_example_files() -> list:676 """List all example .py files in the examples/ directory."""677 examples_dir = os.path.join(os.getcwd(), "examples")678 if not os.path.exists(examples_dir):679 return []680 return [f for f in os.listdir(examples_dir) if f.endswith(".py")]681 682 683def get_sample_code(filename: str) -> str:684 """Return sample Python code from examples/ directory."""685 examples_dir = os.path.join(os.getcwd(), "examples")686 file_path = os.path.join(examples_dir, filename)687 with open(file_path, "r", encoding="utf-8") as f:688 return f.read()689 690 691def generate_all_diagrams(692 python_code: str, filename: str = "diagram"693) -> Tuple[Optional[str], Optional[str], str]:694 """Generate class diagram, call-graph diagram and analysis in one call.695 696 Args:697 python_code: The Python code to analyze698 filename: Base filename for diagrams699 700 Returns:701 Tuple of (uml_diagram_path, call_graph_path, analysis_text)702 """703 if not python_code.strip():704 return None, None, "No code provided for analysis."705 706 print("🚀 Starting comprehensive diagram generation...")707 708 # Step 1: Generate UML Class Diagram709 print("📊 Step 1/3: Generating UML class diagram...")710 uml_diagram_path = generate_diagram(python_code, filename)711 712 # Step 2: Generate Call Graph713 print("🔗 Step 2/3: Generating call graph...")714 try:715 cleanup_testing_space()716 dot_content, call_graph_path, structured_data = generate_call_graph_with_pyan3(717 python_code718 )719 except Exception as e:720 print(f"⚠️ Call graph generation failed: {e}")721 call_graph_path = None722 723 # Step 3: Generate Analysis724 print("📈 Step 3/3: Performing code analysis...")725 analysis_text = analyze_code_structure(python_code)726 727 print("✅ All diagrams and analysis completed!")728 729 return uml_diagram_path, call_graph_path, analysis_text730 731 732# =============================================================================733# ❶ Wrapper functions for diagram and analysis generation734# These will be connected to the UI buttons and the MCP interfaces.735# =============================================================================736 737def generate_class_diagram_only(python_code: str) -> Optional[str]:738 """Generates just the UML class diagram."""739 if not python_code.strip():740 gr.Warning("Input code is empty!")741 return None742 return generate_diagram(python_code)743 744def generate_call_graph_only(python_code: str) -> Optional[str]:745 """Generates just the call graph diagram."""746 if not python_code.strip():747 gr.Warning("Input code is empty!")748 return None749 _, png_path, _ = generate_call_graph_with_pyan3(python_code)750 return png_path751 752def analyze_code_only(python_code: str) -> str:753 """Generates just the code analysis report."""754 if not python_code.strip():755 gr.Warning("Input code is empty!")756 return "No code provided to analyze."757 return analyze_code_structure(python_code)758 759def generate_all_outputs(python_code: str) -> Tuple[Optional[str], Optional[str], str]:760 """Generates all three outputs: UML diagram, call graph, and analysis."""761 if not python_code.strip():762 gr.Warning("Input code is empty!")763 return None, None, "No code provided to analyze."764 765 print("🚀 Starting comprehensive generation...")766 uml_path = generate_diagram(python_code)767 _, call_graph_path, _ = generate_call_graph_with_pyan3(python_code)768 analysis_text = analyze_code_structure(python_code)769 print("✅ All outputs generated!")770 771 return uml_path, call_graph_path, analysis_text772 773# =============================================================================774# ❷ Four MCP-exposed Interfaces775# These are NOT rendered in the UI but are exposed as tools for agents.776# =============================================================================777 778iface_class = gr.Interface(779 fn=generate_class_diagram_only,780 inputs=gr.Textbox(lines=20, label="Python code"),781 outputs=gr.Image(label="UML diagram"),782 api_name="generate_class_diagram",783 description="Create a UML class diagram (PNG) from Python code.",784)785 786iface_call = gr.Interface(787 fn=generate_call_graph_only,788 inputs=gr.Textbox(lines=20, label="Python code"),789 outputs=gr.Image(label="Call‑graph"),790 api_name="generate_call_graph_diagram",791 description="Generate a function‑call graph (PNG) from Python code.",792)793 794iface_analysis = gr.Interface(795 fn=analyze_code_only,796 inputs=gr.Textbox(lines=20, label="Python code"),797 outputs=gr.Markdown(label="Analysis"),798 api_name="analyze_code_structure",799 description="Return a Markdown report with complexity metrics.",800)801 802iface_all = gr.Interface(803 fn=generate_all_outputs,804 inputs=gr.Textbox(lines=20, label="Python code"),805 outputs=[806 gr.Image(label="UML diagram"),807 gr.Image(label="Call‑graph"),808 gr.Markdown(label="Analysis"),809 ],810 api_name="generate_all",811 description="Run class diagram, call graph and analysis in one call.",812)813 814 815# =============================================================================816# ❸ The Cleaned-up Web UI (using gr.Blocks)817# =============================================================================818with gr.Blocks(819 title="Python Code Visualizer & Analyzer",820 theme=gr.themes.Soft(primary_hue="blue"),821 css=""" .gradio-container { max-width: 1400px !important; } """,822) as demo:823 # iface_class = gr.Interface(fn=generate_class_diagram_only, inputs=gr.Textbox(), outputs=gr.Image(), api_name="generate_class_diagram", description="Create a UML class diagram (PNG) from Python code.", visible =False)824 # iface_call = gr.Interface(fn=generate_call_graph_only, inputs=gr.Textbox(), outputs=gr.Image(), api_name="generate_call_graph_diagram", description="Generate a function‑call graph (PNG) from Python code.", visible =False)825 # iface_analysis = gr.Interface(fn=analyze_code_only, inputs=gr.Textbox(), outputs=gr.Markdown(), api_name="analyze_code_structure", description="Return a Markdown report with complexity metrics.", visible =False)826 # iface_all = gr.Interface(fn=generate_all_outputs, inputs=gr.Textbox(), outputs=[gr.Image(), gr.Image(), gr.Markdown()], api_name="generate_all", description="Run class diagram, call graph and analysis in one call.", visible =False)827 gr.Markdown(828 """829 # 🐍 Python Code Visualizer & Analyzer830 **Enter Python code, then choose an action to generate diagrams and analysis.**831 This app also functions as an MCP Server, exposing four tools for AI assistants.832 """833 )834 835 with gr.Row():836 # ---------- Left column – inputs and actions -----------------------------------837 with gr.Column(scale=2):838 gr.Markdown("### 1. Input Code")839 840 example_files = list_example_files()841 print(f"🔍 Found {len(example_files)} example files: {example_files}")842 if example_files:843 example_dropdown = gr.Dropdown(844 label="Load an Example",845 choices=example_files,846 value=example_files[0],847 )848 # initial_code = get_sample_code(example_files[0])849 # initial_code = "# Paste your Python code here\n\nclass MyClass:\n pass"850 initial_code = "Choose an example file from dropdown or paster your python code here "851 # initial_code = get_sample_code("simple_class.py")852 else:853 initial_code = "# Paste your Python code here\n\nclass MyClass:\n pass"854 855 code_input = gr.Textbox(856 label="Python Code",857 placeholder="Paste your Python code here…",858 lines=15,859 max_lines=200,860 value=initial_code,861 elem_classes=["code-input"],862 )863 864 gr.Markdown("### 2. Choose an Action")865 with gr.Row():866 class_btn = gr.Button("🖼️ Generate Class Diagram")867 call_graph_btn = gr.Button("🔗 Generate Call Graph")868 analyze_btn = gr.Button("📈 Analyze Code")869 all_btn = gr.Button("✨ Generate All", variant="primary")870 871 # ---------- Right column – outputs ---------------------------------872 with gr.Column(scale=3):873 gr.Markdown("### 3. Results")874 with gr.Tabs():875 with gr.TabItem("UML Class Diagram"):876 uml_output = gr.Image(label="UML Class Diagram", show_download_button=True, interactive=False)877 with gr.TabItem("Function Call Graph"):878 call_graph_output = gr.Image(label="Function Call Graph", show_download_button=True, interactive=False)879 with gr.TabItem("Code Analysis Report"):880 analysis_output = gr.Markdown(label="Comprehensive Code Analysis", elem_classes=["analysis-output"])881 882 # -------------------------------------------------------------------------883 # Event handlers884 # -------------------------------------------------------------------------885 886 # Handler to load example code when dropdown changes887 if example_files:888 def _load_example(example_filename: str):889 return get_sample_code(example_filename)890 example_dropdown.change(fn=_load_example, inputs=example_dropdown, outputs=code_input, api_name = False)891 892 # Handlers for the four action buttons893 # class_btn.click(894 # fn=generate_class_diagram_only,895 # inputs=[code_input],896 # outputs=[uml_output],897 # api_name=False # Prevents this from creating a duplicate API endpoint898 # )899 class_btn.click(900 fn=generate_class_diagram_only,901 inputs=[code_input],902 outputs=[uml_output],903 # api_name=False # Prevents this from creating a duplicate API endpoint904 )905 906 call_graph_btn.click(907 fn=generate_call_graph_only,908 inputs=[code_input],909 outputs=[call_graph_output],910 # api_name=False911 )912 913 analyze_btn.click(914 fn=analyze_code_only,915 inputs=[code_input],916 outputs=[analysis_output],917 # api_name=False918 )919 920 all_btn.click(921 fn=generate_all_outputs,922 inputs=[code_input],923 outputs=[uml_output, call_graph_output, analysis_output],924 # api_name=False925 )926 927# =============================================================================928# ❹ Launch the App and MCP Server929# =============================================================================930if __name__ == "__main__":931 setup_testing_space() # Create a persistent working dir if needed932 933 demo.launch(934 mcp_server=True, # Enable MCP endpoints (/gradio_api/mcp/*)935 show_api=True, # Expose ONLY the 4 Interfaces as tools936 show_error=True, # Display exceptions in the UI937 debug=True, # Verbose server logs938 share = True,939 )