CoolFace
Apppublic

Agents-MCP-Hackathon/Python-Code-to-Diagram-Generator-MCP

sourceHugging Facemitupdated 1y agoView on Hugging Face
6likes
app.py939 linesDownload Raw Back to root
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    )