| "Gradio UI for DeepBoner agent with MCP server support." |
|
|
| import os |
| from collections.abc import AsyncGenerator |
| from typing import Any, Literal |
|
|
| import gradio as gr |
|
|
| from src.config.domain import ResearchDomain |
| from src.orchestrators import create_orchestrator |
| from src.utils.config import settings |
| from src.utils.exceptions import ConfigurationError |
| from src.utils.models import OrchestratorConfig |
| from src.utils.service_loader import warmup_services |
|
|
| OrchestratorMode = Literal["advanced", "hierarchical"] |
|
|
|
|
| |
| |
| CUSTOM_CSS = """ |
| .api-key-input input { |
| background-color: #1f2937 !important; |
| color: white !important; |
| border-color: #374151 !important; |
| } |
| .api-key-input input:focus, |
| .api-key-input input:focus-visible { |
| background-color: #1f2937 !important; |
| color: white !important; |
| border-color: #e879f9 !important; |
| outline: none !important; |
| } |
| /* Override aggressive browser autofill styling */ |
| .api-key-input input:-webkit-autofill, |
| .api-key-input input:-webkit-autofill:hover, |
| .api-key-input input:-webkit-autofill:focus { |
| -webkit-box-shadow: 0 0 0px 1000px #1f2937 inset !important; |
| -webkit-text-fill-color: white !important; |
| caret-color: white !important; |
| transition: background-color 5000s ease-in-out 0s; |
| } |
| """ |
|
|
|
|
| def configure_orchestrator( |
| use_mock: bool = False, |
| mode: OrchestratorMode = "advanced", |
| user_api_key: str | None = None, |
| domain: str | ResearchDomain | None = None, |
| ) -> tuple[Any, str]: |
| """ |
| Create an orchestrator instance. |
| |
| Unified Architecture (SPEC-16): All users get Advanced Mode. |
| Backend auto-selects: OpenAI (if key) β HuggingFace (free fallback). |
| |
| Args: |
| use_mock: If True, use MockJudgeHandler (no API key needed) |
| mode: Orchestrator mode (default "advanced", "hierarchical" for sub-iteration) |
| user_api_key: Optional user-provided API key (BYOK) - auto-detects provider |
| domain: Research domain (defaults to "sexual_health") |
| |
| Returns: |
| Tuple of (Orchestrator instance, backend_name) |
| """ |
| |
| config = OrchestratorConfig( |
| max_iterations=10, |
| max_results_per_tool=10, |
| ) |
|
|
| backend_info = "Unknown" |
|
|
| |
| if use_mock: |
| backend_info = "Mock (Testing)" |
|
|
| |
| elif user_api_key and user_api_key.strip(): |
| if user_api_key.startswith("sk-"): |
| backend_info = "Paid API (OpenAI)" |
| else: |
| raise ConfigurationError("Invalid API key format. Expected sk-... (OpenAI)") |
|
|
| |
| elif settings.has_openai_key: |
| backend_info = "Paid API (OpenAI from env)" |
|
|
| |
| else: |
| backend_info = "Free Tier (Llama 3.1 / Mistral)" |
|
|
| orchestrator = create_orchestrator( |
| config=config, |
| mode=mode, |
| api_key=user_api_key, |
| domain=domain, |
| ) |
|
|
| return orchestrator, backend_info |
|
|
|
|
| def _validate_inputs( |
| api_key: str | None, |
| api_key_state: str | None, |
| ) -> tuple[str | None, bool]: |
| """Validate inputs and determine key status. |
| |
| Unified Architecture (SPEC-16): Mode is always "advanced". |
| Backend auto-selects based on available API keys. |
| |
| Returns: |
| Tuple of (effective_user_key, has_paid_key) |
| """ |
| |
| user_api_key = (api_key or api_key_state or "").strip() or None |
|
|
| |
| has_openai = settings.has_openai_key |
| has_paid_key = has_openai or bool(user_api_key) |
|
|
| return user_api_key, has_paid_key |
|
|
|
|
| async def research_agent( |
| message: str, |
| history: list[dict[str, Any]], |
| domain: str = "sexual_health", |
| api_key: str = "", |
| api_key_state: str = "", |
| ) -> AsyncGenerator[str, None]: |
| """ |
| Gradio chat function that runs the research agent. |
| |
| Unified Architecture (SPEC-16): Always uses Advanced Mode. |
| Backend auto-selects: OpenAI (if key) β HuggingFace (free fallback). |
| |
| Args: |
| message: User's research question |
| history: Chat history (Gradio format) |
| domain: Research domain |
| api_key: Optional user-provided API key (BYOK - auto-detects provider) |
| api_key_state: Persistent API key state (survives example clicks) |
| |
| Yields: |
| Markdown-formatted responses for streaming |
| """ |
| if not message.strip(): |
| yield "Please enter a research question." |
| return |
|
|
| |
| domain_str = domain or "sexual_health" |
|
|
| |
| user_api_key, has_paid_key = _validate_inputs(api_key, api_key_state) |
|
|
| if not has_paid_key: |
| yield ( |
| "π€ **Free Tier**: Using HuggingFace Inference (Llama 3.1 / Mistral) for AI analysis.\n" |
| "For premium models, enter an OpenAI API key below.\n\n" |
| ) |
|
|
| |
| response_parts: list[str] = [] |
| streaming_buffer = "" |
|
|
| try: |
| |
| |
| orchestrator, backend_name = configure_orchestrator( |
| use_mock=False, |
| mode="advanced", |
| user_api_key=user_api_key, |
| domain=domain_str, |
| ) |
|
|
| |
| |
| domain_display = domain_str.replace("_", " ").title() |
| yield ( |
| f"π§ **Backend**: {backend_name} | **Domain**: {domain_display}\n\n" |
| "β³ **Processing...** Searching PubMed, ClinicalTrials.gov, Europe PMC, OpenAlex...\n" |
| ) |
|
|
| async for event in orchestrator.run(message): |
| |
| |
|
|
| |
| if event.type == "streaming": |
| |
| streaming_buffer += event.message |
| |
| |
| current_parts = [*response_parts, f"π‘ **STREAMING**: {streaming_buffer}"] |
| yield "\n\n".join(current_parts) |
| continue |
|
|
| |
| if streaming_buffer: |
| response_parts.append(f"π‘ **STREAMING**: {streaming_buffer}") |
| streaming_buffer = "" |
|
|
| |
| if event.type == "complete": |
| response_parts.append(event.message) |
| yield "\n\n".join(response_parts) |
| else: |
| |
| event_md = event.to_markdown() |
| response_parts.append(event_md) |
| |
| yield "\n\n".join(response_parts) |
|
|
| |
| if streaming_buffer: |
| response_parts.append(f"π‘ **STREAMING**: {streaming_buffer}") |
| yield "\n\n".join(response_parts) |
|
|
| except Exception as e: |
| yield f"β **Error**: {e!s}" |
|
|
|
|
| def create_demo() -> tuple[gr.ChatInterface, gr.Accordion]: |
| """ |
| Create the Gradio demo interface with MCP support. |
| |
| Returns: |
| Configured Gradio Blocks interface with MCP server enabled |
| """ |
| additional_inputs_accordion = gr.Accordion(label="βοΈ API Key (Free tier works!)", open=False) |
|
|
| |
| api_key_state = gr.State("") |
|
|
| |
| |
| description = ( |
| "<div style='text-align: center;'>" |
| "<em>AI-Powered Research Agent β searches PubMed, " |
| "ClinicalTrials.gov, Europe PMC & OpenAlex</em><br><br>" |
| "Deep research for sexual wellness, ED treatments, hormone therapy, " |
| "libido, and reproductive health - for all genders." |
| "</div>" |
| "<hr style='margin: 1em auto; width: 80%; border: none; " |
| "border-top: 1px solid #374151;'>" |
| "<div style='text-align: center;'>" |
| "<em>Research tool only β not for medical advice.</em><br>" |
| "<strong>MCP Server Active</strong>: Connect Claude Desktop to " |
| "<code>/gradio_api/mcp/</code>" |
| "</div>" |
| ) |
|
|
| demo = gr.ChatInterface( |
| fn=research_agent, |
| title="π DeepBoner", |
| description=description, |
| examples=[ |
| |
| |
| |
| [ |
| "What drugs improve female libido post-menopause?", |
| "sexual_health", |
| None, |
| None, |
| ], |
| [ |
| "Testosterone therapy for hypoactive sexual desire disorder?", |
| "sexual_health", |
| None, |
| None, |
| ], |
| [ |
| "Clinical trials for PDE5 inhibitors alternatives?", |
| "sexual_health", |
| None, |
| None, |
| ], |
| ], |
| |
| |
| cache_examples=False, |
| run_examples_on_click=False, |
| additional_inputs_accordion=additional_inputs_accordion, |
| additional_inputs=[ |
| |
| |
| gr.Dropdown( |
| choices=[d.value for d in ResearchDomain], |
| value="sexual_health", |
| label="Research Domain", |
| info="DeepBoner specializes in sexual health research", |
| visible=False, |
| ), |
| gr.Textbox( |
| label="π API Key (Optional)", |
| placeholder="sk-... (OpenAI)", |
| type="password", |
| info="Leave empty for free tier. Auto-detects provider from key prefix.", |
| elem_classes=["api-key-input"], |
| ), |
| api_key_state, |
| ], |
| ) |
|
|
| return demo, additional_inputs_accordion |
|
|
|
|
| def main() -> None: |
| """Run the Gradio app with MCP server enabled.""" |
| warmup_services() |
| demo, _ = create_demo() |
| demo.launch( |
| server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), |
| server_port=7860, |
| share=False, |
| mcp_server=True, |
| ssr_mode=False, |
| css=CUSTOM_CSS, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|