Spaces:
Sleeping
Sleeping
| # Standard library imports | |
| import os | |
| import io | |
| import asyncio | |
| from pathlib import Path | |
| from typing import Any, Dict, Optional | |
| from contextlib import contextmanager | |
| # Third-party imports | |
| import gradio as gr | |
| from fastapi import FastAPI, File, Form, UploadFile | |
| import uvicorn | |
| import json | |
| # Local imports | |
| from utils import validate_mime_type, format_results_to_mcp | |
| from main import predict, app as fastapi_app | |
| # Check if we're running in Hugging Face Spaces | |
| IS_HF_SPACE = os.getenv("SPACE_ID") is not None | |
| # Store the original working directory | |
| ORIGINAL_CWD = os.getcwd() | |
| def stable_cwd(): | |
| """Context manager to maintain a stable working directory.""" | |
| current_dir = os.getcwd() | |
| try: | |
| os.chdir(ORIGINAL_CWD) | |
| yield | |
| finally: | |
| os.chdir(current_dir) | |
| # Get MCP context with proper path handling | |
| def get_mcp_context_wrapper() -> Dict[str, Any]: | |
| """Wrapper around get_mcp_context that handles Spaces paths.""" | |
| try: | |
| with stable_cwd(): | |
| # In Spaces, we know the file is in the root directory | |
| if IS_HF_SPACE: | |
| context_path = Path(ORIGINAL_CWD) / 'mcp_context.json' | |
| if not context_path.exists(): | |
| raise FileNotFoundError(f"MCP context file not found at {context_path}") | |
| with open(context_path, 'r') as f: | |
| return json.load(f) | |
| # Otherwise use the utility function that searches multiple locations | |
| from utils import get_mcp_context | |
| return get_mcp_context() | |
| except Exception as e: | |
| return { | |
| "error": f"Failed to load MCP context: {str(e)}", | |
| "app": { | |
| "id": "lepanto1571.wrapper.posebusters", | |
| "name": "PoseBusters MCP wrapper", | |
| "version": "0.5" | |
| } | |
| } | |
| async def process_files(ligand_file, protein_file, crystal_file=None): | |
| """Process uploaded files using the robust predict function from main.py.""" | |
| try: | |
| with stable_cwd(): | |
| # Handle file uploads in a way that works in both environments | |
| async def create_upload_file(file_obj) -> Optional[UploadFile]: | |
| if file_obj is None: | |
| return None | |
| try: | |
| # Get the filename, handling both string paths and file-like objects | |
| if hasattr(file_obj, 'orig_name'): | |
| filename = file_obj.orig_name | |
| elif hasattr(file_obj, 'name'): | |
| filename = file_obj.name | |
| else: | |
| filename = str(file_obj) | |
| # Create a UploadFile object that works in both environments | |
| if hasattr(file_obj, 'read'): | |
| # File-like object (e.g. in Spaces or already open file) | |
| content = await file_obj.read() if asyncio.iscoroutinefunction(file_obj.read) else file_obj.read() | |
| if hasattr(file_obj, 'seek'): | |
| file_obj.seek(0) # Reset file pointer for future reads | |
| file_like = io.BytesIO(content if isinstance(content, bytes) else content.encode('utf-8')) | |
| return UploadFile( | |
| file=file_like, | |
| filename=Path(filename).name | |
| ) | |
| elif hasattr(file_obj, 'name') and os.path.exists(file_obj.name): | |
| # Local file path that exists | |
| return UploadFile( | |
| file=open(file_obj.name, "rb"), | |
| filename=Path(filename).name | |
| ) | |
| else: | |
| raise ValueError(f"Unable to read file: {filename}") | |
| except Exception as e: | |
| raise ValueError(f"Failed to process file {getattr(file_obj, 'name', str(file_obj))}: {str(e)}") | |
| # Convert files to UploadFile objects | |
| ligand_input = await create_upload_file(ligand_file) | |
| protein_input = await create_upload_file(protein_file) | |
| crystal_input = await create_upload_file(crystal_file) | |
| if not ligand_input or not protein_input: | |
| raise ValueError("Both ligand and protein files are required") | |
| # Use the action based on whether crystal file is provided | |
| action = "redocking_validation" if crystal_input else "validate_pose" | |
| # Call the robust predict function | |
| return await predict( | |
| action=action, | |
| ligand_input=ligand_input, | |
| protein_input=protein_input, | |
| crystal_input=crystal_input | |
| ) | |
| except Exception as e: | |
| return { | |
| "object_id": "validation_results", | |
| "data": { | |
| "columns": ["ligand_id", "status", "passed/total", "details"], | |
| "rows": [[ | |
| Path(getattr(ligand_file, 'orig_name', | |
| getattr(ligand_file, 'name', 'unknown'))).stem, | |
| "❌", | |
| "0/0", | |
| str(e) | |
| ]] | |
| } | |
| } | |
| theme = gr.themes.Base() | |
| # Main validation interface | |
| validation_ui = gr.Interface( | |
| fn=process_files, | |
| inputs=[ | |
| gr.File(label="Ligand (.sdf)", type="filepath", file_count="single"), | |
| gr.File(label="Protein (.pdb)", type="filepath", file_count="single"), | |
| gr.File(label="Crystal Ligand (.sdf, optional)", type="filepath", file_count="single") | |
| ], | |
| outputs="json", | |
| title="PoseBusters Validation", | |
| description="Upload files to validate ligand–protein structures using PoseBusters.", | |
| theme=theme, | |
| cache_examples=False # Disable caching to prevent file handle issues | |
| ) | |
| # MCP Context viewer | |
| context_ui = gr.Interface( | |
| fn=get_mcp_context_wrapper, | |
| inputs=None, | |
| outputs=gr.JSON(), | |
| title="MCP Context", | |
| description="View the MCP context metadata that defines this tool's capabilities.", | |
| theme=theme, | |
| cache_examples=False # Disable caching to prevent file handle issues | |
| ) | |
| # Combined interface | |
| demo = gr.TabbedInterface( | |
| interface_list=[validation_ui, context_ui], | |
| tab_names=["💊 Validation", "🔍 MCP Context"], | |
| title="PoseBusters MCP Wrapper", | |
| theme=theme | |
| ) | |
| def main(): | |
| """Run the server based on environment.""" | |
| if IS_HF_SPACE: | |
| # In Hugging Face Spaces, just run the Gradio app | |
| demo.launch(server_name="0.0.0.0") | |
| else: | |
| # For local/Docker, mount Gradio into FastAPI and run the server | |
| app = gr.mount_gradio_app(fastapi_app, demo, path="/") | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| if __name__ == "__main__": | |
| main() | |