VibecoderMcSwaggins commited on
Commit
a18a4b3
Β·
unverified Β·
1 Parent(s): ef2b4e3

fix(ui): simplify BYOK settings with auto-detection (#58)

Browse files

* docs: add UI simplification spec for BYOK cleanup

Addresses GitHub issues #52 and #53:
- Remove api_provider dropdown (auto-detect from key prefix)
- Simplify examples table to just query + mode
- Clearer free tier messaging

Spec: docs/bugs/FIX_UI_SIMPLIFICATION.md

* docs: update UI simplification spec with senior review fixes

Incorporates all feedback from senior review:
- Change 6: Fix Advanced mode check (was using removed param)
- Change 7: Remove 'Using your X key' message (would crash)
- Change 8: Remove api_provider from function call
- All 11 changes now explicitly documented with line numbers
- Added GPT-5.1 and Claude Sonnet 4.5 to compatibility matrix
- Added comprehensive testing checklist

* fix(ui): simplify BYOK settings with auto-detection

Removes api_provider dropdown and auto-detects provider from key prefix (sk- vs sk-ant-). Simplifies examples table and clarifies free tier messaging.

Fixes #52, Fixes #53

* fix(ui): remove empty API key column from examples table

* docs: add UI/UX brainstorming for mode selection

Documents current state after PR #58:
- Anthropic key only works for Simple mode (no AnthropicChatClient)
- Advanced mode requires OpenAI key (Magentic limitation)
- Screenshot analysis showing current UI issues
- Decision needed: remove Anthropic or keep?

* fix(ui): apply CodeRabbit review suggestions

- Use ConfigurationError instead of ValueError for invalid API key
- Add env var override for server_name with nosec B104 comment

docs/brainstorming/UI_MODE_SELECTION_UX.md ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UI/UX Brainstorm: Mode Selection & API Key Experience
2
+
3
+ **Date**: 2025-11-28
4
+ **Status**: IMPLEMENTED (2025-11-28)
5
+ **Related**: Issues #52, #53, PR #58
6
+
7
+ ---
8
+
9
+ ## CRITICAL FINDING: Anthropic Key is Nearly Useless
10
+
11
+ **Code verification** (2025-11-28):
12
+ ```
13
+ grep -r "AnthropicChatClient" src/ β†’ NO RESULTS
14
+ grep -r "OpenAIChatClient" src/ β†’ 22 RESULTS (all Magentic agents)
15
+ ```
16
+
17
+ The `agent-framework` package (Microsoft's Magentic) **ONLY** has `OpenAIChatClient`.
18
+ There is no `AnthropicChatClient`. This means:
19
+
20
+ | Feature | OpenAI Key | Anthropic Key |
21
+ |---------|------------|---------------|
22
+ | Simple mode (Judge LLM) | βœ… GPT-5.1 | βœ… Claude Sonnet 4.5 |
23
+ | Advanced mode (Multi-agent) | βœ… Full orchestration | ❌ **DOES NOT WORK** |
24
+ | Value proposition | Full access | Simple mode only |
25
+
26
+ **Decision**: Keep Anthropic support for Simple mode, but ensure UX clearly differentiates capabilities.
27
+
28
+ ---
29
+
30
+ ## Current State (After PR #58)
31
+
32
+ ### What Users See (Screenshot 2025-11-28)
33
+
34
+ ```
35
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
36
+ β”‚ ≑ Examples β”‚
37
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
38
+ β”‚ β”‚ Orchestrator Mode β”‚
39
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
40
+ β”‚ What drugs improve female libido post-menopause? β”‚ simple β”‚
41
+ β”‚ Clinical trials for erectile dysfunction altern... β”‚ advanced β”‚
42
+ β”‚ Evidence for testosterone therapy in women with... β”‚ simple β”‚
43
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
44
+
45
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
46
+ β”‚ βš™οΈ Mode & API Key (Free tier works!) [β–Ό] β”‚
47
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
48
+ β”‚ β”‚
49
+ β”‚ Orchestrator Mode β”‚
50
+ β”‚ ⚑ Simple: Fast (Free/Any Key) | πŸ”¬ Advanced: Deep Multi-Agent (OpenAI Key Only) β”‚
51
+ β”‚ [● simple] [β—‹ advanced] β”‚
52
+ β”‚ β”‚
53
+ β”‚ πŸ”‘ API Key (Optional) β”‚
54
+ β”‚ Leave empty for free tier. Auto-detects provider from key prefix. β”‚
55
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
56
+ β”‚ β”‚ sk-... (OpenAI) or sk-ant-... (Anthropic) β”‚ β”‚
57
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
58
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
59
+ ```
60
+
61
+ ### Observations from Screenshot
62
+
63
+ 1. **Examples table**: 2 columns (Query + Mode) - clean, one example now shows "advanced" βœ…
64
+ 2. **One example shows "advanced"**: Improves discoverability of Advanced mode βœ…
65
+ 3. **Accordion collapsed by default**: Still collapsed, but with more inviting label βœ…
66
+ 4. **Placeholder mentions Anthropic**: Correct, but now clearly tied to Simple mode only via info text βœ…
67
+ 5. **"Advanced: Requires OpenAI key"**: Now more prominent with emojis and clearer phrasing in info text βœ…
68
+
69
+ ### The Two Modes
70
+
71
+ | Mode | Backend | Capabilities | Requirements |
72
+ |------|---------|--------------|--------------|
73
+ | **Simple** | Linear orchestrator | Search β†’ Judge β†’ Report (single pass) | None (free tier) or any API key |
74
+ | **Advanced** | Magentic multi-agent | SearchAgent, JudgeAgent, HypothesisAgent, ReportAgent working together with iterative refinement | **OpenAI API key only** |
75
+
76
+ ---
77
+
78
+ ## Problems Identified (Addressed)
79
+
80
+ ### P1: Advanced Mode is Hidden β†’ ADDRESSED
81
+ - **Fix**: One example now shows "advanced" mode.
82
+ - **Fix**: Accordion label is more descriptive.
83
+
84
+ ### P2: Mode/Key Relationship is Unclear β†’ ADDRESSED
85
+ - **Fix**: `gr.Radio` info text clearly states "OpenAI Key Only" for Advanced mode, using emojis for emphasis.
86
+
87
+ ### P3: No Incentive to Try Advanced β†’ PARTIALLY ADDRESSED
88
+ - **Fix**: Emojis and "Deep Multi-Agent" hint at the value. Further marketing/documentation still needed for full "wow" moment.
89
+
90
+ ### P4: Anthropic Users Left Out β†’ ADDRESSED (Clarified)
91
+ - **Fix**: Anthropic keys still work for Simple mode, and the info text clarifies the limitation for Advanced mode.
92
+
93
+ ---
94
+
95
+ ## Options to Consider (Decision Made)
96
+
97
+ The recommendation of **Modified Option A (Better Education + Examples)** with slight modification to accordion label was implemented.
98
+
99
+ ---
100
+
101
+ ## Implementation Notes (Completed)
102
+
103
+ ```python
104
+ # From src/app.py
105
+ examples=[
106
+ ["What drugs improve female libido post-menopause?", "simple"],
107
+ ["Clinical trials for erectile dysfunction alternatives to PDE5 inhibitors?", "advanced"], # Changed
108
+ ["Evidence for testosterone therapy in women with HSDD?", "simple"],
109
+ ],
110
+
111
+ additional_inputs_accordion=gr.Accordion(
112
+ label="βš™οΈ Mode & API Key (Free tier works!)", # Changed
113
+ open=False
114
+ ),
115
+
116
+ gr.Radio(
117
+ choices=["simple", "advanced"],
118
+ value="simple",
119
+ label="Orchestrator Mode",
120
+ info=( # Changed
121
+ "⚑ Simple: Fast (Free/Any Key) | "
122
+ "πŸ”¬ Advanced: Deep Multi-Agent (OpenAI Key Only)"
123
+ ),
124
+ ),
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Decision Log
130
+
131
+ | Date | Decision | Rationale |
132
+ |------|----------|-----------|
133
+ | 2025-11-28 | Implemented Modified Option A | Minimal changes, high impact on discoverability, graceful fallback, user-approved accordion label. |
docs/bugs/FIX_UI_SIMPLIFICATION.md ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UI Simplification: Remove API Provider Dropdown
2
+
3
+ **Issues**: #52, #53
4
+ **Priority**: P1 - UX improvement for hackathon demo
5
+ **Estimated Time**: 30 minutes
6
+ **Senior Review**: βœ… Approved with changes (incorporated below)
7
+
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The current UI has confusing BYOK (Bring Your Own Key) settings:
13
+
14
+ 1. **Provider dropdown is misleading** - Shows "openai" but actually uses free tier when no key
15
+ 2. **Examples table shows useless columns** - API Key (empty), Provider (ignored)
16
+ 3. **Anthropic doesn't work with Advanced mode** - Only OpenAI has `agent-framework` support
17
+
18
+ ## Solution
19
+
20
+ Remove `api_provider` dropdown entirely. Auto-detect provider from key prefix.
21
+
22
+ **Functionality preserved:**
23
+ - Simple mode: Free tier, OpenAI, OR Anthropic (all work)
24
+ - Advanced mode: OpenAI only (Magentic multi-agent requires `OpenAIChatClient`)
25
+
26
+ ---
27
+
28
+ ## Implementation
29
+
30
+ ### File: `src/app.py`
31
+
32
+ #### Change 1: Update `configure_orchestrator()` signature (lines 23-28)
33
+
34
+ ```python
35
+ # BEFORE
36
+ def configure_orchestrator(
37
+ use_mock: bool = False,
38
+ mode: str = "simple",
39
+ user_api_key: str | None = None,
40
+ api_provider: str = "openai", # ← REMOVE
41
+ ) -> tuple[Any, str]:
42
+
43
+ # AFTER
44
+ def configure_orchestrator(
45
+ use_mock: bool = False,
46
+ mode: str = "simple",
47
+ user_api_key: str | None = None,
48
+ ) -> tuple[Any, str]:
49
+ ```
50
+
51
+ #### Change 2: Update docstring (lines 29-40)
52
+
53
+ ```python
54
+ # AFTER
55
+ """
56
+ Create an orchestrator instance.
57
+
58
+ Args:
59
+ use_mock: If True, use MockJudgeHandler (no API key needed)
60
+ mode: Orchestrator mode ("simple" or "advanced")
61
+ user_api_key: Optional user-provided API key (BYOK) - auto-detects provider
62
+
63
+ Returns:
64
+ Tuple of (Orchestrator instance, backend_name)
65
+ """
66
+ ```
67
+
68
+ #### Change 3: Replace provider logic with auto-detection (lines 62-88)
69
+
70
+ ```python
71
+ # BEFORE (lines 62-88) - complex provider checking with api_provider param
72
+
73
+ # AFTER - auto-detect from key prefix
74
+ # 2. Paid API Key (User provided or Env)
75
+ elif user_api_key and user_api_key.strip():
76
+ # Auto-detect provider from key prefix
77
+ model: AnthropicModel | OpenAIModel
78
+ if user_api_key.startswith("sk-ant-"):
79
+ # Anthropic key
80
+ anthropic_provider = AnthropicProvider(api_key=user_api_key)
81
+ model = AnthropicModel(settings.anthropic_model, provider=anthropic_provider)
82
+ backend_info = "Paid API (Anthropic)"
83
+ elif user_api_key.startswith("sk-"):
84
+ # OpenAI key
85
+ openai_provider = OpenAIProvider(api_key=user_api_key)
86
+ model = OpenAIModel(settings.openai_model, provider=openai_provider)
87
+ backend_info = "Paid API (OpenAI)"
88
+ else:
89
+ raise ValueError(
90
+ "Invalid API key format. Expected sk-... (OpenAI) or sk-ant-... (Anthropic)"
91
+ )
92
+ judge_handler = JudgeHandler(model=model)
93
+
94
+ # 3. Environment API Keys (fallback)
95
+ elif os.getenv("OPENAI_API_KEY"):
96
+ judge_handler = JudgeHandler(model=None) # Uses env key
97
+ backend_info = "Paid API (OpenAI from env)"
98
+
99
+ elif os.getenv("ANTHROPIC_API_KEY"):
100
+ judge_handler = JudgeHandler(model=None) # Uses env key
101
+ backend_info = "Paid API (Anthropic from env)"
102
+
103
+ # 4. Free Tier (HuggingFace Inference)
104
+ else:
105
+ judge_handler = HFInferenceJudgeHandler()
106
+ backend_info = "Free Tier (Llama 3.1 / Mistral)"
107
+ ```
108
+
109
+ #### Change 4: Update `research_agent()` signature (lines 105-111)
110
+
111
+ ```python
112
+ # BEFORE
113
+ async def research_agent(
114
+ message: str,
115
+ history: list[dict[str, Any]],
116
+ mode: str = "simple",
117
+ api_key: str = "",
118
+ api_provider: str = "openai", # ← REMOVE
119
+ ) -> AsyncGenerator[str, None]:
120
+
121
+ # AFTER
122
+ async def research_agent(
123
+ message: str,
124
+ history: list[dict[str, Any]],
125
+ mode: str = "simple",
126
+ api_key: str = "",
127
+ ) -> AsyncGenerator[str, None]:
128
+ ```
129
+
130
+ #### Change 5: Update docstring (lines 112-124)
131
+
132
+ ```python
133
+ # AFTER
134
+ """
135
+ Gradio chat function that runs the research agent.
136
+
137
+ Args:
138
+ message: User's research question
139
+ history: Chat history (Gradio format)
140
+ mode: Orchestrator mode ("simple" or "advanced")
141
+ api_key: Optional user-provided API key (BYOK - auto-detects provider)
142
+
143
+ Yields:
144
+ Markdown-formatted responses for streaming
145
+ """
146
+ ```
147
+
148
+ #### Change 6: Fix Advanced mode check (line 139)
149
+
150
+ ```python
151
+ # BEFORE
152
+ if mode == "advanced" and not (has_openai or (has_user_key and api_provider == "openai")):
153
+
154
+ # AFTER - auto-detect OpenAI key from prefix
155
+ is_openai_user_key = user_api_key and user_api_key.startswith("sk-") and not user_api_key.startswith("sk-ant-")
156
+ if mode == "advanced" and not (has_openai or is_openai_user_key):
157
+ yield (
158
+ "⚠️ **Advanced mode requires OpenAI API key.** "
159
+ "Anthropic keys only work in Simple mode. Falling back to Simple.\n\n"
160
+ )
161
+ mode = "simple"
162
+ ```
163
+
164
+ #### Change 7: Remove premature "Using your key" message (lines 146-151)
165
+
166
+ ```python
167
+ # BEFORE - uses api_provider which no longer exists
168
+ if has_user_key:
169
+ yield (
170
+ f"πŸ”‘ **Using your {api_provider.upper()} API key** - "
171
+ "Your key is used only for this session and is never stored.\n\n"
172
+ )
173
+
174
+ # AFTER - remove this block entirely
175
+ # The backend_name from configure_orchestrator already shows "Paid API (OpenAI)" or "Paid API (Anthropic)"
176
+ # No need for duplicate messaging
177
+ ```
178
+
179
+ #### Change 8: Update configure_orchestrator call (lines 165-170)
180
+
181
+ ```python
182
+ # BEFORE
183
+ orchestrator, backend_name = configure_orchestrator(
184
+ use_mock=False,
185
+ mode=mode,
186
+ user_api_key=user_api_key,
187
+ api_provider=api_provider, # ← REMOVE
188
+ )
189
+
190
+ # AFTER
191
+ orchestrator, backend_name = configure_orchestrator(
192
+ use_mock=False,
193
+ mode=mode,
194
+ user_api_key=user_api_key,
195
+ )
196
+ ```
197
+
198
+ #### Change 9: Simplify examples (lines 210-229)
199
+
200
+ ```python
201
+ # BEFORE - 4 items per example
202
+ examples=[
203
+ ["What drugs improve female libido post-menopause?", "simple", "", "openai"],
204
+ ["Clinical trials for erectile dysfunction alternatives to PDE5 inhibitors?", "simple", "", "openai"],
205
+ ["Evidence for testosterone therapy in women with HSDD?", "simple", "", "openai"],
206
+ ],
207
+
208
+ # AFTER - 2 items per example (query, mode) - API key always empty in examples
209
+ examples=[
210
+ ["What drugs improve female libido post-menopause?", "simple"],
211
+ ["Clinical trials for ED alternatives to PDE5 inhibitors?", "simple"],
212
+ ["Evidence for testosterone therapy in women with HSDD?", "simple"],
213
+ ],
214
+ ```
215
+
216
+ #### Change 10: Update additional_inputs (lines 231-252)
217
+
218
+ ```python
219
+ # BEFORE - 3 inputs (mode, api_key, api_provider)
220
+ additional_inputs=[
221
+ gr.Radio(
222
+ choices=["simple", "advanced"],
223
+ value="simple",
224
+ label="Orchestrator Mode",
225
+ info="Simple: Linear (Free Tier Friendly) | Advanced: Multi-Agent (Requires OpenAI)",
226
+ ),
227
+ gr.Textbox(
228
+ label="πŸ”‘ API Key (Optional - BYOK)",
229
+ placeholder="sk-... or sk-ant-...",
230
+ type="password",
231
+ info="Enter your own API key. Never stored.",
232
+ ),
233
+ gr.Radio( # ← REMOVE THIS ENTIRE BLOCK
234
+ choices=["openai", "anthropic"],
235
+ value="openai",
236
+ label="API Provider",
237
+ info="Select the provider for your API key",
238
+ ),
239
+ ],
240
+
241
+ # AFTER - 2 inputs (mode, api_key)
242
+ additional_inputs=[
243
+ gr.Radio(
244
+ choices=["simple", "advanced"],
245
+ value="simple",
246
+ label="Orchestrator Mode",
247
+ info="Simple: Works with any key or free tier | Advanced: Requires OpenAI key",
248
+ ),
249
+ gr.Textbox(
250
+ label="πŸ”‘ API Key (Optional)",
251
+ placeholder="sk-... (OpenAI) or sk-ant-... (Anthropic)",
252
+ type="password",
253
+ info="Leave empty for free tier. Auto-detects provider from key prefix.",
254
+ ),
255
+ ],
256
+ ```
257
+
258
+ #### Change 11: Update accordion label (line 230)
259
+
260
+ ```python
261
+ # BEFORE
262
+ additional_inputs_accordion=gr.Accordion(label="βš™οΈ Settings", open=False),
263
+
264
+ # AFTER
265
+ additional_inputs_accordion=gr.Accordion(label="βš™οΈ Settings (Free tier works without API key)", open=False),
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Testing Checklist
271
+
272
+ ### Manual Tests
273
+ - [ ] **No key**: Shows "Free Tier (Llama 3.1 / Mistral)" in backend
274
+ - [ ] **OpenAI key (sk-...)**: Shows "Paid API (OpenAI)" in backend
275
+ - [ ] **Anthropic key (sk-ant-...)**: Shows "Paid API (Anthropic)" in backend
276
+ - [ ] **Invalid key format**: Shows error message
277
+ - [ ] **Anthropic key + Advanced mode**: Falls back to Simple with warning
278
+ - [ ] **OpenAI key + Advanced mode**: Uses full Magentic multi-agent
279
+ - [ ] **Examples table**: Shows only 2 columns (query, mode)
280
+ - [ ] **MCP server**: Still accessible at `/gradio_api/mcp/`
281
+
282
+ ### Unit Test Updates
283
+ - [ ] `tests/unit/test_app_smoke.py` - may need update if checking input count
284
+
285
+ ---
286
+
287
+ ## Definition of Done
288
+
289
+ - [ ] `api_provider` parameter removed from `configure_orchestrator()`
290
+ - [ ] `api_provider` parameter removed from `research_agent()`
291
+ - [ ] Auto-detection logic works for `sk-` and `sk-ant-` prefixes
292
+ - [ ] Advanced mode check uses auto-detection (not removed param)
293
+ - [ ] "Using your X key" message removed (backend_name handles this)
294
+ - [ ] Examples table shows 2 columns
295
+ - [ ] Accordion label updated
296
+ - [ ] Placeholder text shows both key formats
297
+ - [ ] All existing tests pass
298
+ - [ ] MCP server still works
299
+
300
+ ---
301
+
302
+ ## Mode Compatibility Matrix (Unchanged)
303
+
304
+ | Mode | No Key | OpenAI Key | Anthropic Key |
305
+ |------|--------|------------|---------------|
306
+ | **Simple** | βœ… Free tier | βœ… GPT-5.1 | βœ… Claude Sonnet 4.5 |
307
+ | **Advanced** | ⚠️ Falls back | βœ… Full Magentic | ⚠️ Falls back to Simple |
308
+
309
+ ---
310
+
311
+ ## Related
312
+ - Issue #52: UI Polish - Examples table confusion
313
+ - Issue #53: API Provider Simplification
314
+ - Senior Review: Approved 2025-11-28
src/app.py CHANGED
@@ -17,6 +17,7 @@ from src.tools.europepmc import EuropePMCTool
17
  from src.tools.pubmed import PubMedTool
18
  from src.tools.search_handler import SearchHandler
19
  from src.utils.config import settings
 
20
  from src.utils.models import OrchestratorConfig
21
 
22
 
@@ -24,7 +25,6 @@ def configure_orchestrator(
24
  use_mock: bool = False,
25
  mode: str = "simple",
26
  user_api_key: str | None = None,
27
- api_provider: str = "openai",
28
  ) -> tuple[Any, str]:
29
  """
30
  Create an orchestrator instance.
@@ -32,8 +32,7 @@ def configure_orchestrator(
32
  Args:
33
  use_mock: If True, use MockJudgeHandler (no API key needed)
34
  mode: Orchestrator mode ("simple" or "advanced")
35
- user_api_key: Optional user-provided API key (BYOK)
36
- api_provider: API provider ("openai" or "anthropic")
37
 
38
  Returns:
39
  Tuple of (Orchestrator instance, backend_name)
@@ -60,34 +59,35 @@ def configure_orchestrator(
60
  backend_info = "Mock (Testing)"
61
 
62
  # 2. Paid API Key (User provided or Env)
63
- elif (
64
- user_api_key
65
- or (api_provider == "openai" and os.getenv("OPENAI_API_KEY"))
66
- or (api_provider == "anthropic" and os.getenv("ANTHROPIC_API_KEY"))
67
- ):
68
- model: AnthropicModel | OpenAIModel | None = None
69
- if user_api_key:
70
- # Validate key/provider match to prevent silent auth failures
71
- if api_provider == "openai" and user_api_key.startswith("sk-ant-"):
72
- raise ValueError("Anthropic key provided but OpenAI provider selected")
73
- is_openai_key = user_api_key.startswith("sk-") and not user_api_key.startswith(
74
- "sk-ant-"
75
- )
76
- if api_provider == "anthropic" and is_openai_key:
77
- raise ValueError("OpenAI key provided but Anthropic provider selected")
78
- if api_provider == "anthropic":
79
- anthropic_provider = AnthropicProvider(api_key=user_api_key)
80
- model = AnthropicModel(settings.anthropic_model, provider=anthropic_provider)
81
- elif api_provider == "openai":
82
- openai_provider = OpenAIProvider(api_key=user_api_key)
83
- model = OpenAIModel(settings.openai_model, provider=openai_provider)
84
- backend_info = f"Paid API ({api_provider.upper()})"
85
  else:
86
- backend_info = "Paid API (Env Config)"
87
-
 
88
  judge_handler = JudgeHandler(model=model)
89
 
90
- # 3. Free Tier (HuggingFace Inference)
 
 
 
 
 
 
 
 
 
91
  else:
92
  judge_handler = HFInferenceJudgeHandler()
93
  backend_info = "Free Tier (Llama 3.1 / Mistral)"
@@ -107,7 +107,6 @@ async def research_agent(
107
  history: list[dict[str, Any]],
108
  mode: str = "simple",
109
  api_key: str = "",
110
- api_provider: str = "openai",
111
  ) -> AsyncGenerator[str, None]:
112
  """
113
  Gradio chat function that runs the research agent.
@@ -116,8 +115,7 @@ async def research_agent(
116
  message: User's research question
117
  history: Chat history (Gradio format)
118
  mode: Orchestrator mode ("simple" or "advanced")
119
- api_key: Optional user-provided API key (BYOK - Bring Your Own Key)
120
- api_provider: API provider ("openai" or "anthropic")
121
 
122
  Yields:
123
  Markdown-formatted responses for streaming
@@ -132,24 +130,22 @@ async def research_agent(
132
  # Check available keys
133
  has_openai = bool(os.getenv("OPENAI_API_KEY"))
134
  has_anthropic = bool(os.getenv("ANTHROPIC_API_KEY"))
135
- has_user_key = bool(user_api_key)
136
- has_paid_key = has_openai or has_anthropic or has_user_key
 
 
 
137
 
138
  # Advanced mode requires OpenAI specifically (due to agent-framework binding)
139
- if mode == "advanced" and not (has_openai or (has_user_key and api_provider == "openai")):
140
  yield (
141
  "⚠️ **Warning**: Advanced mode currently requires OpenAI API key. "
142
- "Falling back to simple mode.\n\n"
143
  )
144
  mode = "simple"
145
 
146
- # Inform user about their key being used
147
- if has_user_key:
148
- yield (
149
- f"πŸ”‘ **Using your {api_provider.upper()} API key** - "
150
- "Your key is used only for this session and is never stored.\n\n"
151
- )
152
- elif not has_paid_key:
153
  # No paid keys - will use FREE HuggingFace Inference
154
  yield (
155
  "πŸ€— **Free Tier**: Using HuggingFace Inference (Llama 3.1 / Mistral) for AI analysis.\n"
@@ -166,7 +162,6 @@ async def research_agent(
166
  use_mock=False, # Never use mock in production - HF Inference is the free fallback
167
  mode=mode,
168
  user_api_key=user_api_key,
169
- api_provider=api_provider,
170
  )
171
 
172
  yield f"🧠 **Backend**: {backend_name}\n\n"
@@ -187,13 +182,16 @@ async def research_agent(
187
  yield f"❌ **Error**: {e!s}"
188
 
189
 
190
- def create_demo() -> gr.ChatInterface:
191
  """
192
  Create the Gradio demo interface with MCP support.
193
 
194
  Returns:
195
  Configured Gradio Blocks interface with MCP server enabled
196
  """
 
 
 
197
  # 1. Unwrapped ChatInterface (Fixes Accordion Bug)
198
  demo = gr.ChatInterface(
199
  fn=research_agent,
@@ -211,55 +209,44 @@ def create_demo() -> gr.ChatInterface:
211
  [
212
  "What drugs improve female libido post-menopause?",
213
  "simple",
214
- "",
215
- "openai",
216
  ],
217
  [
218
  "Clinical trials for erectile dysfunction alternatives to PDE5 inhibitors?",
219
- "simple",
220
- "",
221
- "openai",
222
  ],
223
  [
224
  "Evidence for testosterone therapy in women with HSDD?",
225
  "simple",
226
- "",
227
- "openai",
228
  ],
229
  ],
230
- additional_inputs_accordion=gr.Accordion(label="βš™οΈ Settings", open=False),
231
  additional_inputs=[
232
  gr.Radio(
233
  choices=["simple", "advanced"],
234
  value="simple",
235
  label="Orchestrator Mode",
236
  info=(
237
- "Simple: Linear (Free Tier Friendly) | Advanced: Multi-Agent (Requires OpenAI)"
 
238
  ),
239
  ),
240
  gr.Textbox(
241
- label="πŸ”‘ API Key (Optional - BYOK)",
242
- placeholder="sk-... or sk-ant-...",
243
  type="password",
244
- info="Enter your own API key. Never stored.",
245
- ),
246
- gr.Radio(
247
- choices=["openai", "anthropic"],
248
- value="openai",
249
- label="API Provider",
250
- info="Select the provider for your API key",
251
  ),
252
  ],
253
  )
254
 
255
- return demo
256
 
257
 
258
  def main() -> None:
259
  """Run the Gradio app with MCP server enabled."""
260
- demo = create_demo()
261
  demo.launch(
262
- server_name="0.0.0.0",
263
  server_port=7860,
264
  share=False,
265
  mcp_server=True,
 
17
  from src.tools.pubmed import PubMedTool
18
  from src.tools.search_handler import SearchHandler
19
  from src.utils.config import settings
20
+ from src.utils.exceptions import ConfigurationError
21
  from src.utils.models import OrchestratorConfig
22
 
23
 
 
25
  use_mock: bool = False,
26
  mode: str = "simple",
27
  user_api_key: str | None = None,
 
28
  ) -> tuple[Any, str]:
29
  """
30
  Create an orchestrator instance.
 
32
  Args:
33
  use_mock: If True, use MockJudgeHandler (no API key needed)
34
  mode: Orchestrator mode ("simple" or "advanced")
35
+ user_api_key: Optional user-provided API key (BYOK) - auto-detects provider
 
36
 
37
  Returns:
38
  Tuple of (Orchestrator instance, backend_name)
 
59
  backend_info = "Mock (Testing)"
60
 
61
  # 2. Paid API Key (User provided or Env)
62
+ elif user_api_key and user_api_key.strip():
63
+ # Auto-detect provider from key prefix
64
+ model: AnthropicModel | OpenAIModel
65
+ if user_api_key.startswith("sk-ant-"):
66
+ # Anthropic key
67
+ anthropic_provider = AnthropicProvider(api_key=user_api_key)
68
+ model = AnthropicModel(settings.anthropic_model, provider=anthropic_provider)
69
+ backend_info = "Paid API (Anthropic)"
70
+ elif user_api_key.startswith("sk-"):
71
+ # OpenAI key
72
+ openai_provider = OpenAIProvider(api_key=user_api_key)
73
+ model = OpenAIModel(settings.openai_model, provider=openai_provider)
74
+ backend_info = "Paid API (OpenAI)"
 
 
 
 
 
 
 
 
 
75
  else:
76
+ raise ConfigurationError(
77
+ "Invalid API key format. Expected sk-... (OpenAI) or sk-ant-... (Anthropic)"
78
+ )
79
  judge_handler = JudgeHandler(model=model)
80
 
81
+ # 3. Environment API Keys (fallback)
82
+ elif os.getenv("OPENAI_API_KEY"):
83
+ judge_handler = JudgeHandler(model=None) # Uses env key
84
+ backend_info = "Paid API (OpenAI from env)"
85
+
86
+ elif os.getenv("ANTHROPIC_API_KEY"):
87
+ judge_handler = JudgeHandler(model=None) # Uses env key
88
+ backend_info = "Paid API (Anthropic from env)"
89
+
90
+ # 4. Free Tier (HuggingFace Inference)
91
  else:
92
  judge_handler = HFInferenceJudgeHandler()
93
  backend_info = "Free Tier (Llama 3.1 / Mistral)"
 
107
  history: list[dict[str, Any]],
108
  mode: str = "simple",
109
  api_key: str = "",
 
110
  ) -> AsyncGenerator[str, None]:
111
  """
112
  Gradio chat function that runs the research agent.
 
115
  message: User's research question
116
  history: Chat history (Gradio format)
117
  mode: Orchestrator mode ("simple" or "advanced")
118
+ api_key: Optional user-provided API key (BYOK - auto-detects provider)
 
119
 
120
  Yields:
121
  Markdown-formatted responses for streaming
 
130
  # Check available keys
131
  has_openai = bool(os.getenv("OPENAI_API_KEY"))
132
  has_anthropic = bool(os.getenv("ANTHROPIC_API_KEY"))
133
+ # Check for OpenAI user key
134
+ is_openai_user_key = (
135
+ user_api_key and user_api_key.startswith("sk-") and not user_api_key.startswith("sk-ant-")
136
+ )
137
+ has_paid_key = has_openai or has_anthropic or bool(user_api_key)
138
 
139
  # Advanced mode requires OpenAI specifically (due to agent-framework binding)
140
+ if mode == "advanced" and not (has_openai or is_openai_user_key):
141
  yield (
142
  "⚠️ **Warning**: Advanced mode currently requires OpenAI API key. "
143
+ "Anthropic keys only work in Simple mode. Falling back to Simple.\n\n"
144
  )
145
  mode = "simple"
146
 
147
+ # Inform user about fallback if no keys
148
+ if not has_paid_key:
 
 
 
 
 
149
  # No paid keys - will use FREE HuggingFace Inference
150
  yield (
151
  "πŸ€— **Free Tier**: Using HuggingFace Inference (Llama 3.1 / Mistral) for AI analysis.\n"
 
162
  use_mock=False, # Never use mock in production - HF Inference is the free fallback
163
  mode=mode,
164
  user_api_key=user_api_key,
 
165
  )
166
 
167
  yield f"🧠 **Backend**: {backend_name}\n\n"
 
182
  yield f"❌ **Error**: {e!s}"
183
 
184
 
185
+ def create_demo() -> tuple[gr.ChatInterface, gr.Accordion]:
186
  """
187
  Create the Gradio demo interface with MCP support.
188
 
189
  Returns:
190
  Configured Gradio Blocks interface with MCP server enabled
191
  """
192
+ additional_inputs_accordion = gr.Accordion(
193
+ label="βš™οΈ Mode & API Key (Free tier works!)", open=False
194
+ )
195
  # 1. Unwrapped ChatInterface (Fixes Accordion Bug)
196
  demo = gr.ChatInterface(
197
  fn=research_agent,
 
209
  [
210
  "What drugs improve female libido post-menopause?",
211
  "simple",
 
 
212
  ],
213
  [
214
  "Clinical trials for erectile dysfunction alternatives to PDE5 inhibitors?",
215
+ "advanced",
 
 
216
  ],
217
  [
218
  "Evidence for testosterone therapy in women with HSDD?",
219
  "simple",
 
 
220
  ],
221
  ],
222
+ additional_inputs_accordion=additional_inputs_accordion,
223
  additional_inputs=[
224
  gr.Radio(
225
  choices=["simple", "advanced"],
226
  value="simple",
227
  label="Orchestrator Mode",
228
  info=(
229
+ "⚑ Simple: Fast (Free/Any Key) | "
230
+ "πŸ”¬ Advanced: Deep Multi-Agent (OpenAI Key Only)"
231
  ),
232
  ),
233
  gr.Textbox(
234
+ label="πŸ”‘ API Key (Optional)",
235
+ placeholder="sk-... (OpenAI) or sk-ant-... (Anthropic)",
236
  type="password",
237
+ info="Leave empty for free tier. Auto-detects provider from key prefix.",
 
 
 
 
 
 
238
  ),
239
  ],
240
  )
241
 
242
+ return demo, additional_inputs_accordion
243
 
244
 
245
  def main() -> None:
246
  """Run the Gradio app with MCP server enabled."""
247
+ demo, _ = create_demo()
248
  demo.launch(
249
+ server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), # nosec B104
250
  server_port=7860,
251
  share=False,
252
  mcp_server=True,
tests/unit/test_ui_elements.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from src.app import create_demo
4
+
5
+
6
+ def test_examples_include_advanced_mode():
7
+ """Verify that one example entry uses 'advanced' mode."""
8
+ demo, _ = create_demo()
9
+ assert any(
10
+ "advanced" == example[1] for example in demo.examples
11
+ ), "Expected at least one example to be 'advanced' mode"
12
+
13
+
14
+ def test_accordion_label_updated():
15
+ """Verify the accordion label reflects the new, concise text."""
16
+ _, accordion = create_demo()
17
+ assert (
18
+ accordion.label == "βš™οΈ Mode & API Key (Free tier works!)"
19
+ ), "Accordion label not updated to 'βš™οΈ Mode & API Key (Free tier works!)'"
20
+
21
+
22
+ def test_orchestrator_mode_info_text_updated():
23
+ """Verify the Orchestrator Mode info text contains the new emojis and phrasing."""
24
+ demo, _ = create_demo()
25
+ # Assuming additional_inputs is a list and the Radio is the first element
26
+ orchestrator_radio = demo.additional_inputs[0]
27
+ expected_info = (
28
+ "⚑ Simple: Fast (Free/Any Key) | " "πŸ”¬ Advanced: Deep Multi-Agent " "(OpenAI Key Only)"
29
+ )
30
+ assert isinstance(
31
+ orchestrator_radio, gr.Radio
32
+ ), "Expected first additional input to be gr.Radio"
33
+ assert (
34
+ orchestrator_radio.info == expected_info
35
+ ), "Orchestrator Mode info text not updated correctly"