File size: 8,928 Bytes
5d12635
25c3a8b
 
 
 
 
 
4d87419
 
 
 
25c3a8b
7fab6d4
1922dbd
4e2ccbf
2f8ae1f
25c3a8b
 
f5747b1
da82e7e
25c3a8b
 
 
4d87419
 
 
 
7fab6d4
25c3a8b
 
 
 
 
b2929fc
6ea5a8b
25c3a8b
 
7fab6d4
25c3a8b
36983ae
 
72b2667
36983ae
 
 
25c3a8b
 
2f8ae1f
36983ae
25c3a8b
 
7fab6d4
 
 
 
 
25c3a8b
 
7fab6d4
 
 
6ea5a8b
 
 
 
 
 
 
 
 
 
 
 
 
7fab6d4
da82e7e
6ea5a8b
 
4d87419
25c3a8b
6ea5a8b
 
 
 
 
 
 
 
 
 
7fab6d4
 
 
 
 
25c3a8b
 
 
1922dbd
d1f91a4
25c3a8b
 
7fab6d4
 
25c3a8b
 
 
 
1922dbd
4d87419
25c3a8b
 
 
 
 
 
 
b2929fc
6ea5a8b
25c3a8b
 
 
 
 
 
 
 
4d87419
 
 
7fab6d4
506a9c0
 
6ea5a8b
 
 
 
 
1922dbd
b2929fc
6ea5a8b
1922dbd
b2929fc
6ea5a8b
1922dbd
 
 
6ea5a8b
 
7fab6d4
4d87419
7fab6d4
 
4d87419
 
25c3a8b
506a9c0
25c3a8b
 
7fab6d4
 
 
 
4d87419
 
 
7fab6d4
 
 
25c3a8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b16e7a5
25c3a8b
10e234c
25c3a8b
 
10e234c
25c3a8b
b16e7a5
 
 
fcc601a
 
 
5d12635
fcc601a
5d12635
fcc601a
5d12635
 
fcc601a
 
 
 
 
b16e7a5
 
 
 
 
 
 
 
 
 
 
 
fcc601a
b16e7a5
fcc601a
 
b2929fc
fcc601a
 
87feaa7
fcc601a
 
6ea5a8b
 
fcc601a
6ea5a8b
fcc601a
 
 
25c3a8b
b16e7a5
25c3a8b
 
 
10e234c
b16e7a5
25c3a8b
da82e7e
25c3a8b
 
1812a2a
420d8ba
25c3a8b
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
"""Gradio UI for DeepBoner agent with MCP server support."""

import os
from collections.abc import AsyncGenerator
from typing import Any

import gradio as gr
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.anthropic import AnthropicProvider
from pydantic_ai.providers.openai import OpenAIProvider

from src.agent_factory.judges import HFInferenceJudgeHandler, JudgeHandler, MockJudgeHandler
from src.orchestrator_factory import create_orchestrator
from src.tools.clinicaltrials import ClinicalTrialsTool
from src.tools.europepmc import EuropePMCTool
from src.tools.pubmed import PubMedTool
from src.tools.search_handler import SearchHandler
from src.utils.config import settings
from src.utils.exceptions import ConfigurationError
from src.utils.models import OrchestratorConfig


def configure_orchestrator(
    use_mock: bool = False,
    mode: str = "simple",
    user_api_key: str | None = None,
) -> tuple[Any, str]:
    """
    Create an orchestrator instance.

    Args:
        use_mock: If True, use MockJudgeHandler (no API key needed)
        mode: Orchestrator mode ("simple" or "advanced")
        user_api_key: Optional user-provided API key (BYOK) - auto-detects provider

    Returns:
        Tuple of (Orchestrator instance, backend_name)
    """
    # Create orchestrator config
    config = OrchestratorConfig(
        max_iterations=10,
        max_results_per_tool=10,
    )

    # Create search tools
    search_handler = SearchHandler(
        tools=[PubMedTool(), ClinicalTrialsTool(), EuropePMCTool()],
        timeout=config.search_timeout,
    )

    # Create judge (mock, real, or free tier)
    judge_handler: JudgeHandler | MockJudgeHandler | HFInferenceJudgeHandler
    backend_info = "Unknown"

    # 1. Forced Mock (Unit Testing)
    if use_mock:
        judge_handler = MockJudgeHandler()
        backend_info = "Mock (Testing)"

    # 2. Paid API Key (User provided or Env)
    elif user_api_key and user_api_key.strip():
        # Auto-detect provider from key prefix
        model: AnthropicModel | OpenAIModel
        if user_api_key.startswith("sk-ant-"):
            # Anthropic key
            anthropic_provider = AnthropicProvider(api_key=user_api_key)
            model = AnthropicModel(settings.anthropic_model, provider=anthropic_provider)
            backend_info = "Paid API (Anthropic)"
        elif user_api_key.startswith("sk-"):
            # OpenAI key
            openai_provider = OpenAIProvider(api_key=user_api_key)
            model = OpenAIModel(settings.openai_model, provider=openai_provider)
            backend_info = "Paid API (OpenAI)"
        else:
            raise ConfigurationError(
                "Invalid API key format. Expected sk-... (OpenAI) or sk-ant-... (Anthropic)"
            )
        judge_handler = JudgeHandler(model=model)

    # 3. Environment API Keys (fallback)
    elif os.getenv("OPENAI_API_KEY"):
        judge_handler = JudgeHandler(model=None)  # Uses env key
        backend_info = "Paid API (OpenAI from env)"

    elif os.getenv("ANTHROPIC_API_KEY"):
        judge_handler = JudgeHandler(model=None)  # Uses env key
        backend_info = "Paid API (Anthropic from env)"

    # 4. Free Tier (HuggingFace Inference)
    else:
        judge_handler = HFInferenceJudgeHandler()
        backend_info = "Free Tier (Llama 3.1 / Mistral)"

    orchestrator = create_orchestrator(
        search_handler=search_handler,
        judge_handler=judge_handler,
        config=config,
        mode=mode,  # type: ignore
        api_key=user_api_key,
    )

    return orchestrator, backend_info


async def research_agent(
    message: str,
    history: list[dict[str, Any]],
    mode: str = "simple",
    api_key: str = "",
) -> AsyncGenerator[str, None]:
    """
    Gradio chat function that runs the research agent.

    Args:
        message: User's research question
        history: Chat history (Gradio format)
        mode: Orchestrator mode ("simple" or "advanced")
        api_key: Optional user-provided API key (BYOK - auto-detects provider)

    Yields:
        Markdown-formatted responses for streaming
    """
    if not message.strip():
        yield "Please enter a research question."
        return

    # Clean user-provided API key
    user_api_key = api_key.strip() if api_key else None

    # Check available keys
    has_openai = bool(os.getenv("OPENAI_API_KEY"))
    has_anthropic = bool(os.getenv("ANTHROPIC_API_KEY"))
    # Check for OpenAI user key
    is_openai_user_key = (
        user_api_key and user_api_key.startswith("sk-") and not user_api_key.startswith("sk-ant-")
    )
    has_paid_key = has_openai or has_anthropic or bool(user_api_key)

    # Advanced mode requires OpenAI specifically (due to agent-framework binding)
    if mode == "advanced" and not (has_openai or is_openai_user_key):
        yield (
            "⚠️ **Warning**: Advanced mode currently requires OpenAI API key. "
            "Anthropic keys only work in Simple mode. Falling back to Simple.\n\n"
        )
        mode = "simple"

    # Inform user about fallback if no keys
    if not has_paid_key:
        # No paid keys - will use FREE HuggingFace Inference
        yield (
            "πŸ€— **Free Tier**: Using HuggingFace Inference (Llama 3.1 / Mistral) for AI analysis.\n"
            "For premium models, enter an OpenAI or Anthropic API key below.\n\n"
        )

    # Run the agent and stream events
    response_parts: list[str] = []

    try:
        # use_mock=False - let configure_orchestrator decide based on available keys
        # It will use: Paid API > HF Inference (free tier)
        orchestrator, backend_name = configure_orchestrator(
            use_mock=False,  # Never use mock in production - HF Inference is the free fallback
            mode=mode,
            user_api_key=user_api_key,
        )

        yield f"🧠 **Backend**: {backend_name}\n\n"

        async for event in orchestrator.run(message):
            # Format event as markdown
            event_md = event.to_markdown()
            response_parts.append(event_md)

            # If complete, show full response
            if event.type == "complete":
                yield event.message
            else:
                # Show progress
                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="βš™οΈ Mode & API Key (Free tier works!)", open=False
    )
    # 1. Unwrapped ChatInterface (Fixes Accordion Bug)
    demo = gr.ChatInterface(
        fn=research_agent,
        title="πŸ† DeepBoner",
        description=(
            "*AI-Powered Sexual Health Research Agent β€” searches PubMed, "
            "ClinicalTrials.gov & Europe PMC*\n\n"
            "Deep research for sexual wellness, ED treatments, hormone therapy, "
            "libido, and reproductive health - for all genders.\n\n"
            "---\n"
            "*Research tool only β€” not for medical advice.*  \n"
            "**MCP Server Active**: Connect Claude Desktop to `/gradio_api/mcp/`"
        ),
        examples=[
            [
                "What drugs improve female libido post-menopause?",
                "simple",
            ],
            [
                "Clinical trials for erectile dysfunction alternatives to PDE5 inhibitors?",
                "advanced",
            ],
            [
                "Evidence for testosterone therapy in women with HSDD?",
                "simple",
            ],
        ],
        additional_inputs_accordion=additional_inputs_accordion,
        additional_inputs=[
            gr.Radio(
                choices=["simple", "advanced"],
                value="simple",
                label="Orchestrator Mode",
                info="⚑ Simple: Free/OpenAI/Anthropic | πŸ”¬ Advanced: OpenAI only",
            ),
            gr.Textbox(
                label="πŸ”‘ API Key (Optional)",
                placeholder="sk-... (OpenAI) or sk-ant-... (Anthropic)",
                type="password",
                info="Leave empty for free tier. Auto-detects provider from key prefix.",
            ),
        ],
    )

    return demo, additional_inputs_accordion


def main() -> None:
    """Run the Gradio app with MCP server enabled."""
    demo, _ = create_demo()
    demo.launch(
        server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),  # nosec B104
        server_port=7860,
        share=False,
        mcp_server=True,
        ssr_mode=False,  # Fix for intermittent loading/hydration issues in HF Spaces
    )


if __name__ == "__main__":
    main()