Spaces:
Sleeping
Sleeping
| """ | |
| Gradio Web UI for Ada to Python Converter with Langfuse instrumentation | |
| """ | |
| import gradio as gr | |
| import tempfile | |
| import time | |
| import threading | |
| import os | |
| from pathlib import Path | |
| from typing import Optional, Tuple, Generator, List, Dict, Any | |
| from loguru import logger | |
| from .agents.model_factory import create_model | |
| from .agents.orchestrator_agent import OrchestratorAgent | |
| from .utils.zip_utils import ZipHandler | |
| from .utils.project_converter import AdaProjectConverter | |
| from .logger.config import configure_logging | |
| # Langfuse instrumentation using OpenTelemetry | |
| try: | |
| from langfuse import get_client | |
| LANGFUSE_AVAILABLE = True | |
| except ImportError: | |
| LANGFUSE_AVAILABLE = False | |
| class AdaConverterUI: | |
| """Gradio UI for Ada to Python conversion.""" | |
| def __init__(self): | |
| configure_logging(log_level="INFO", log_to_file=False) | |
| self.model = None | |
| self.zip_handler = ZipHandler() | |
| self.current_conversion_results = None | |
| self.current_extracted_path = None | |
| # Initialize Langfuse client if available | |
| self.langfuse_client = None | |
| if LANGFUSE_AVAILABLE: | |
| self._setup_langfuse() | |
| # Initialize model | |
| try: | |
| self.model = create_model() | |
| self.orchestrator = OrchestratorAgent(self.model) | |
| logger.info("Model initialized successfully for UI") | |
| except Exception as e: | |
| logger.error("Failed to initialize model for UI", error=str(e)) | |
| raise | |
| def _setup_langfuse(self): | |
| """Setup Langfuse client for the UI using OpenTelemetry""" | |
| try: | |
| langfuse_secret_key = os.getenv("LANGFUSE_SECRET_KEY") | |
| langfuse_public_key = os.getenv("LANGFUSE_PUBLIC_KEY") | |
| langfuse_host = os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com") | |
| if langfuse_secret_key and langfuse_public_key: | |
| # Set environment variables for automatic client initialization | |
| os.environ["LANGFUSE_SECRET_KEY"] = langfuse_secret_key | |
| os.environ["LANGFUSE_PUBLIC_KEY"] = langfuse_public_key | |
| os.environ["LANGFUSE_HOST"] = langfuse_host | |
| # Initialize the client using the new OpenTelemetry-based SDK | |
| self.langfuse_client = get_client() | |
| logger.info("Langfuse client initialized for Web UI with OpenTelemetry") | |
| else: | |
| logger.info("Langfuse not configured for Web UI") | |
| except Exception as e: | |
| logger.warning("Failed to setup Langfuse client", error=str(e)) | |
| def convert_single_ada_file(self, file_path: str) -> Tuple[str, str, str]: | |
| """ | |
| Convert a single Ada file to Python using the orchestrator. | |
| Args: | |
| file_path: Path to the Ada file to convert | |
| Returns: | |
| Tuple of (analysis_markdown, python_code, unit_test_code) | |
| """ | |
| # Start Langfuse span if available | |
| if LANGFUSE_AVAILABLE and self.langfuse_client: | |
| with self.langfuse_client.start_as_current_span(name="convert_single_ada_file") as span: | |
| return self._convert_single_ada_file_impl(file_path, span) | |
| else: | |
| return self._convert_single_ada_file_impl(file_path, None) | |
| def _convert_single_ada_file_impl(self, file_path: str, span) -> Tuple[str, str, str]: | |
| """Implementation of single Ada file conversion with optional Langfuse tracing.""" | |
| try: | |
| file_path = Path(file_path) | |
| # Add Langfuse metadata | |
| if span: | |
| span.update( | |
| input=str(file_path), | |
| metadata={ | |
| "file_path": str(file_path), | |
| "file_extension": file_path.suffix.lower(), | |
| "interface": "web_ui", | |
| "operation": "single_file_conversion" | |
| } | |
| ) | |
| # Check if it's an Ada file | |
| if not file_path.suffix.lower() in ['.ads', '.adb', '.ada']: | |
| error_msg = "β Selected file is not an Ada source file" | |
| if span: | |
| span.update( | |
| output=error_msg, | |
| level="ERROR" | |
| ) | |
| return error_msg, "", "" | |
| # Check if file exists | |
| if not file_path.exists(): | |
| error_msg = "β Selected file does not exist" | |
| if span: | |
| span.update( | |
| output=error_msg, | |
| level="ERROR" | |
| ) | |
| return error_msg, "", "" | |
| # Read the Ada code | |
| try: | |
| ada_code = file_path.read_text(encoding='utf-8') | |
| except UnicodeDecodeError: | |
| try: | |
| ada_code = file_path.read_text(encoding='latin-1') | |
| except Exception as e: | |
| error_msg = f"β Could not read file: {str(e)}" | |
| if span: | |
| span.update( | |
| output=error_msg, | |
| level="ERROR" | |
| ) | |
| return error_msg, "", "" | |
| logger.info("Converting single Ada file", file_path=str(file_path), code_length=len(ada_code)) | |
| # Update Langfuse with Ada code metadata | |
| if span: | |
| span.update( | |
| metadata={ | |
| "file_path": str(file_path), | |
| "file_extension": file_path.suffix.lower(), | |
| "ada_code_length": len(ada_code), | |
| "ada_lines": ada_code.count('\n') + 1, | |
| "interface": "web_ui", | |
| "operation": "single_file_conversion" | |
| } | |
| ) | |
| # Use orchestrator to convert | |
| analysis, python_code, unit_tests = self.orchestrator.convert_ada_to_python(ada_code) | |
| logger.success("Single Ada file converted successfully", file_path=str(file_path)) | |
| # Update Langfuse with results | |
| if span: | |
| span.update( | |
| output={ | |
| "analysis_length": len(analysis), | |
| "python_code_length": len(python_code), | |
| "unit_tests_length": len(unit_tests), | |
| "success": True | |
| } | |
| ) | |
| return analysis, python_code, unit_tests | |
| except Exception as e: | |
| error_msg = f"β **Conversion Failed**\n\nError: {str(e)}" | |
| logger.exception("Failed to convert single Ada file", file_path=file_path, error=str(e)) | |
| # Update Langfuse with error | |
| if span: | |
| span.update( | |
| output=error_msg, | |
| level="ERROR" | |
| ) | |
| return error_msg, "", "" | |
| def validate_zip_file(self, file_path: Optional[str]) -> Tuple[bool, str]: | |
| """ | |
| Validate that the uploaded file is a valid ZIP containing Ada files. | |
| Args: | |
| file_path: Path to the uploaded file | |
| Returns: | |
| Tuple of (is_valid, message) | |
| """ | |
| if not file_path: | |
| self.current_extracted_path = None | |
| return False, "No file uploaded" | |
| file_path = Path(file_path) | |
| # Check if it's a ZIP file | |
| if not self.zip_handler.is_zip_file(str(file_path)): | |
| self.current_extracted_path = None | |
| return False, f"β Invalid file: Must be a ZIP file (got: {file_path.suffix})" | |
| try: | |
| # Try to extract and check for Ada files | |
| ada_project_path, session_dir = self.zip_handler.extract_ada_project(str(file_path)) | |
| ada_files = list(Path(ada_project_path).rglob("*.ad[s|b]")) + list(Path(ada_project_path).rglob("*.ada")) | |
| if not ada_files: | |
| self.current_extracted_path = None | |
| return False, "β No Ada files found in ZIP archive" | |
| # Store the extracted path for file explorer | |
| self.current_extracted_path = ada_project_path | |
| return True, f"β Valid Ada project ZIP with {len(ada_files)} Ada files" | |
| except Exception as e: | |
| self.current_extracted_path = None | |
| return False, f"β Error validating ZIP: {str(e)}" | |
| def convert_ada_project(self, file_path: str, progress: gr.Progress) -> Generator[Tuple[str, str, bool, Optional[str]], None, None]: | |
| """ | |
| Convert Ada project from ZIP file with progress updates. | |
| Args: | |
| file_path: Path to the uploaded ZIP file | |
| progress: Gradio progress tracker | |
| Yields: | |
| Tuples of (status_message, progress_text, download_enabled, download_file) | |
| """ | |
| # Start Langfuse span if available | |
| if LANGFUSE_AVAILABLE and self.langfuse_client: | |
| with self.langfuse_client.start_as_current_span(name="convert_ada_project") as span: | |
| span.update( | |
| input=file_path, | |
| metadata={ | |
| "file_path": file_path, | |
| "interface": "web_ui", | |
| "operation": "project_conversion" | |
| } | |
| ) | |
| yield from self._convert_ada_project_impl(file_path, progress, span) | |
| else: | |
| yield from self._convert_ada_project_impl(file_path, progress, None) | |
| def _convert_ada_project_impl(self, file_path: str, progress: gr.Progress, span) -> Generator[Tuple[str, str, bool, Optional[str]], None, None]: | |
| """Implementation of Ada project conversion with optional Langfuse tracing.""" | |
| try: | |
| # Validate file first | |
| is_valid, validation_msg = self.validate_zip_file(file_path) | |
| if not is_valid: | |
| yield validation_msg, "β Validation failed", False, None | |
| return | |
| yield validation_msg, "π Validation complete", False, None | |
| time.sleep(0.5) # Brief pause for UI feedback | |
| # Extract the Ada project | |
| progress(0.1, desc="Extracting Ada project...") | |
| yield "ποΈ Extracting Ada project from ZIP...", "π¦ Extracting...", False, None | |
| ada_project_path, session_dir = self.zip_handler.extract_ada_project(str(file_path)) | |
| progress(0.2, desc="Setting up conversion...") | |
| yield "βοΈ Setting up conversion process...", "βοΈ Initializing...", False, None | |
| # Create project converter | |
| project_converter = AdaProjectConverter(self.model) | |
| # Start conversion with progress tracking | |
| output_base = f"{Path(file_path).stem}_python" | |
| generator = project_converter.convert_project(ada_project_path, output_base) | |
| # Track conversion progress | |
| try: | |
| while True: | |
| conv_progress, message = next(generator) | |
| # Map conversion progress (0-100) to UI progress (0.2-0.9) | |
| ui_progress = 0.2 + (conv_progress / 100.0) * 0.7 | |
| progress(ui_progress, desc=message) | |
| yield f"π {message}", f"[{conv_progress:5.1f}%] {message}", False, None | |
| except StopIteration as e: | |
| # Conversion completed, get results | |
| result = e.value | |
| self.current_conversion_results = result | |
| progress(0.95, desc="Creating Python project ZIP...") | |
| yield "π¦ Creating Python project ZIP file...", "π¦ Packaging...", False, None | |
| # Create ZIP file for the Python project | |
| python_zip_path = self.zip_handler.create_python_project_zip(result['python_project']) | |
| progress(1.0, desc="Conversion completed!") | |
| # Final success message | |
| success_msg = f""" | |
| π **Conversion Completed Successfully!** | |
| π **Results:** | |
| - β Successfully converted: {result['successful_conversions']}/{result['total_files']} files | |
| - π Python project: `{Path(result['python_project']).name}` | |
| - π¦ ZIP file ready for download | |
| π **Next Steps:** | |
| 1. Download the Python project ZIP file below | |
| 2. Extract and explore the converted Python code | |
| 3. Run tests with: `pytest tests/ -v` | |
| """ | |
| yield success_msg, "β Complete!", True, python_zip_path | |
| except Exception as e: | |
| logger.exception("Conversion failed in UI", error=str(e)) | |
| error_msg = f"β **Conversion Failed**\n\nError: {str(e)}\n\nPlease check your ZIP file and try again." | |
| # Update Langfuse with error | |
| if span: | |
| span.update( | |
| output=error_msg, | |
| level="ERROR" | |
| ) | |
| yield error_msg, "β Failed", False, None | |
| def handle_file_upload(self, file_path: str) -> Tuple[str, str, bool, Optional[str]]: | |
| """ | |
| Handle file upload and validation. | |
| Args: | |
| file_path: Path to uploaded file | |
| Returns: | |
| Tuple of (status_message, progress_text, start_button_enabled, extracted_path) | |
| """ | |
| if not file_path: | |
| return "Please upload a ZIP file containing an Ada project", "", False, None | |
| is_valid, message = self.validate_zip_file(file_path) | |
| if is_valid and self.current_extracted_path: | |
| return message, "Ready to convert", True, self.current_extracted_path | |
| else: | |
| return message, "Invalid file", False, None | |
| def create_interface(self) -> gr.Interface: | |
| """Create the Gradio interface.""" | |
| with gr.Blocks( | |
| title="Ada to Python Converter", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .gradio-container { | |
| max-width: 900px !important; | |
| } | |
| .upload-container { | |
| border: 2px dashed #ccc; | |
| border-radius: 10px; | |
| padding: 20px; | |
| text-align: center; | |
| background-color: #f9f9f9; | |
| } | |
| """ | |
| ) as interface: | |
| gr.Markdown(""" | |
| # π Ada to Python Converter | |
| Upload a ZIP file containing your Ada project and convert it to Python with comprehensive unit tests. | |
| ## How to use: | |
| 1. **Upload** a ZIP file containing `.ads`, `.adb`, or `.ada` files | |
| 2. **Start** the conversion process | |
| 3. **Download** the converted Python project ZIP when ready | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # File upload with ZIP restriction | |
| file_upload = gr.File( | |
| label="π Upload Ada Project ZIP File", | |
| file_types=[".zip"], | |
| file_count="single", | |
| elem_classes=["upload-container"] | |
| ) | |
| # Status message | |
| status_display = gr.Markdown("Please upload a ZIP file to begin") | |
| # Progress indicator | |
| progress_text = gr.Textbox( | |
| label="Progress", | |
| value="", | |
| interactive=False, | |
| visible=True | |
| ) | |
| with gr.Column(scale=1): | |
| # Start conversion button | |
| start_button = gr.Button( | |
| "π Start Conversion", | |
| variant="primary", | |
| interactive=False, | |
| size="lg" | |
| ) | |
| # Download button | |
| download_button = gr.File( | |
| label="π₯ Download Python Project", | |
| visible=False, | |
| interactive=False | |
| ) | |
| # File explorer section | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### π Extracted Project Structure") | |
| file_explorer = gr.FileExplorer( | |
| root_dir="/", # Will be updated dynamically | |
| label="Browse Ada Project Files", | |
| visible=False, | |
| interactive=True, | |
| file_count="single", | |
| height=400 | |
| ) | |
| # Selected file display | |
| selected_file_display = gr.Textbox( | |
| label="Selected File", | |
| value="No file selected", | |
| interactive=False, | |
| visible=False | |
| ) | |
| # Single file conversion results (tabbed interface) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### π Single File Conversion Results") | |
| with gr.Tabs(visible=False) as conversion_tabs: | |
| with gr.Tab("π Analysis"): | |
| analysis_output = gr.Markdown("", label="Analysis Report") | |
| with gr.Tab("π Python Code"): | |
| python_output = gr.Code("", language="python", label="Generated Python Code") | |
| with gr.Tab("π§ͺ Unit Tests"): | |
| tests_output = gr.Code("", language="python", label="Generated Unit Tests") | |
| # Conversion output area (for project conversion) | |
| with gr.Row(): | |
| conversion_output = gr.Markdown( | |
| "", | |
| label="Project Conversion Output", | |
| visible=True | |
| ) | |
| # Event handlers | |
| def update_ui_on_upload(file_path): | |
| status_msg, progress_msg, button_enabled, extracted_path = self.handle_file_upload(file_path) | |
| # Show file explorer if we have valid extracted path | |
| show_explorer = extracted_path is not None | |
| return [ | |
| status_msg, | |
| progress_msg, | |
| gr.update(interactive=button_enabled), | |
| gr.update(visible=False), # Hide download button | |
| gr.update(visible=show_explorer, root_dir=extracted_path if extracted_path else "/"), # Update file explorer | |
| gr.update(visible=show_explorer, value="No file selected" if show_explorer else "") # Update selected file display | |
| ] | |
| def run_conversion(file_path, progress=gr.Progress()): | |
| if not file_path: | |
| return "Please upload a file first", "", gr.update(interactive=False), gr.update(visible=False) | |
| # Generator for progress updates | |
| for status_msg, progress_msg, download_enabled, download_file in self.convert_ada_project(file_path, progress): | |
| yield [ | |
| status_msg, | |
| progress_msg, | |
| gr.update(interactive=False), # Keep button disabled during conversion | |
| gr.update(visible=download_enabled, value=download_file if download_enabled else None) | |
| ] | |
| def on_file_select(selected_file): | |
| """Handle file selection in the explorer""" | |
| if not selected_file: | |
| return [ | |
| "No file selected", | |
| gr.update(visible=False), # Hide conversion tabs | |
| "", # Clear analysis | |
| "", # Clear Python code | |
| "" # Clear unit tests | |
| ] | |
| file_path = Path(selected_file) | |
| display_msg = f"Selected: {file_path.name}" | |
| # Check if it's an Ada file | |
| if file_path.suffix.lower() in ['.ads', '.adb', '.ada']: | |
| display_msg += " (Converting...)" | |
| # Convert the Ada file | |
| analysis, python_code, unit_tests = self.convert_single_ada_file(selected_file) | |
| display_msg = f"Selected: {file_path.name} β Converted" | |
| return [ | |
| display_msg, | |
| gr.update(visible=True), # Show conversion tabs | |
| analysis, # Update analysis | |
| python_code, # Update Python code | |
| unit_tests # Update unit tests | |
| ] | |
| else: | |
| return [ | |
| display_msg + " (Not an Ada file)", | |
| gr.update(visible=False), # Hide conversion tabs | |
| "", # Clear analysis | |
| "", # Clear Python code | |
| "" # Clear unit tests | |
| ] | |
| # Wire up events | |
| file_upload.change( | |
| fn=update_ui_on_upload, | |
| inputs=[file_upload], | |
| outputs=[status_display, progress_text, start_button, download_button, file_explorer, selected_file_display] | |
| ) | |
| # Wire up file explorer selection | |
| file_explorer.change( | |
| fn=on_file_select, | |
| inputs=[file_explorer], | |
| outputs=[selected_file_display, conversion_tabs, analysis_output, python_output, tests_output] | |
| ) | |
| start_button.click( | |
| fn=run_conversion, | |
| inputs=[file_upload], | |
| outputs=[conversion_output, progress_text, start_button, download_button] | |
| ) | |
| return interface | |
| def launch_ui(share: bool = False, server_port: int = 7860): | |
| """Launch the Gradio UI.""" | |
| try: | |
| ui = AdaConverterUI() | |
| interface = ui.create_interface() | |
| print("π Starting Ada to Python Converter Web UI...") | |
| print(f"π‘ Server will be available at: http://localhost:{server_port}") | |
| if share: | |
| print("π Public URL will be generated (share=True)") | |
| interface.launch( | |
| share=share, | |
| server_port=server_port, | |
| server_name="0.0.0.0", | |
| show_api=False, | |
| favicon_path=None | |
| ) | |
| except Exception as e: | |
| logger.exception("Failed to launch UI", error=str(e)) | |
| print(f"β Failed to launch UI: {e}") | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Launch Ada to Python Converter Web UI") | |
| parser.add_argument("--share", action="store_true", help="Create public URL (default: False)") | |
| parser.add_argument("--port", type=int, default=7860, help="Server port (default: 7860)") | |
| args = parser.parse_args() | |
| launch_ui(share=args.share, server_port=args.port) |