Commit
·
29a6844
1
Parent(s):
8f45b69
fix: handle None parameters from Gradio example caching (P0)
Browse filesRoot cause: Gradio passes None for missing example columns during
startup caching, overriding Python default values. Line 131 called
.strip() on None, crashing the HuggingFace Space.
Fix: Add defensive None handling before .strip():
api_key_str = api_key or ""
api_key_state_str = api_key_state or ""
Added tests to prevent regression.
- docs/bugs/P0_GRADIO_EXAMPLE_CACHING_CRASH.md +134 -0
- src/app.py +6 -1
- tests/unit/test_gradio_crash.py +86 -0
docs/bugs/P0_GRADIO_EXAMPLE_CACHING_CRASH.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# P0 Bug Report: Gradio Example Caching Crash
|
| 2 |
+
|
| 3 |
+
## Status
|
| 4 |
+
- **Date:** 2025-11-29
|
| 5 |
+
- **Priority:** P0 CRITICAL (Production Down)
|
| 6 |
+
- **Component:** `src/app.py:131`
|
| 7 |
+
- **Environment:** HuggingFace Spaces (Python 3.11, Gradio)
|
| 8 |
+
|
| 9 |
+
## Error Message
|
| 10 |
+
|
| 11 |
+
```text
|
| 12 |
+
AttributeError: 'NoneType' object has no attribute 'strip'
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
## Full Stack Trace
|
| 16 |
+
|
| 17 |
+
```text
|
| 18 |
+
File "/app/src/app.py", line 131, in research_agent
|
| 19 |
+
user_api_key = (api_key.strip() or api_key_state.strip()) or None
|
| 20 |
+
^^^^^^^^^^^^^
|
| 21 |
+
AttributeError: 'NoneType' object has no attribute 'strip'
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## Root Cause Analysis
|
| 25 |
+
|
| 26 |
+
### The Trigger
|
| 27 |
+
Gradio's example caching mechanism runs the `research_agent` function during startup to pre-cache example outputs. This happens at:
|
| 28 |
+
|
| 29 |
+
```text
|
| 30 |
+
File "/usr/local/lib/python3.11/site-packages/gradio/helpers.py", line 509, in _start_caching
|
| 31 |
+
await self.cache()
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### The Problem
|
| 35 |
+
Our examples only provide values for 2 of the 4 function parameters:
|
| 36 |
+
|
| 37 |
+
```python
|
| 38 |
+
examples=[
|
| 39 |
+
["What is the evidence for testosterone therapy in women with HSDD?", "simple"],
|
| 40 |
+
["Promising drug candidates for endometriosis pain management", "simple"],
|
| 41 |
+
]
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
These map to `[message, mode]` but **NOT** to `api_key` or `api_key_state`.
|
| 45 |
+
|
| 46 |
+
When Gradio runs the function for caching, it passes `None` for the unprovided parameters:
|
| 47 |
+
|
| 48 |
+
```python
|
| 49 |
+
async def research_agent(
|
| 50 |
+
message: str, # ✅ Provided by example
|
| 51 |
+
history: list[...], # ✅ Empty list default
|
| 52 |
+
mode: str = "simple", # ✅ Provided by example
|
| 53 |
+
api_key: str = "", # ❌ Becomes None during caching!
|
| 54 |
+
api_key_state: str = "" # ❌ Becomes None during caching!
|
| 55 |
+
) -> AsyncGenerator[...]:
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### The Crash
|
| 59 |
+
Line 131 attempts to call `.strip()` on `None`:
|
| 60 |
+
|
| 61 |
+
```python
|
| 62 |
+
user_api_key = (api_key.strip() or api_key_state.strip()) or None
|
| 63 |
+
# ^^^^^^^^^^^^^
|
| 64 |
+
# NoneType has no attribute 'strip'
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## Gradio Warning (Ignored)
|
| 68 |
+
|
| 69 |
+
Gradio actually warned us about this:
|
| 70 |
+
|
| 71 |
+
```text
|
| 72 |
+
UserWarning: Examples will be cached but not all input components have
|
| 73 |
+
example values. This may result in an exception being thrown by your function.
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## Solution
|
| 77 |
+
|
| 78 |
+
### Option A: Defensive None Handling (Recommended)
|
| 79 |
+
Add None guards before calling `.strip()`:
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
# Handle None values from Gradio example caching
|
| 83 |
+
api_key_str = api_key or ""
|
| 84 |
+
api_key_state_str = api_key_state or ""
|
| 85 |
+
user_api_key = (api_key_str.strip() or api_key_state_str.strip()) or None
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Option B: Disable Example Caching
|
| 89 |
+
Set `cache_examples=False` in ChatInterface:
|
| 90 |
+
|
| 91 |
+
```python
|
| 92 |
+
gr.ChatInterface(
|
| 93 |
+
fn=research_agent,
|
| 94 |
+
examples=[...],
|
| 95 |
+
cache_examples=False, # Disable caching
|
| 96 |
+
)
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
This avoids the crash but loses the UX benefit of pre-cached examples.
|
| 100 |
+
|
| 101 |
+
### Option C: Provide Full Example Values
|
| 102 |
+
Include all 4 columns in examples:
|
| 103 |
+
|
| 104 |
+
```python
|
| 105 |
+
examples=[
|
| 106 |
+
["What is the evidence...", "simple", "", ""], # [msg, mode, api_key, state]
|
| 107 |
+
]
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
This is verbose and exposes internal state to users.
|
| 111 |
+
|
| 112 |
+
## Recommendation
|
| 113 |
+
|
| 114 |
+
**Option A** is the cleanest fix. It:
|
| 115 |
+
1. Maintains cached examples for fast UX
|
| 116 |
+
2. Handles edge cases defensively
|
| 117 |
+
3. Doesn't expose internal state in examples
|
| 118 |
+
|
| 119 |
+
## Pre-Merge Checklist
|
| 120 |
+
|
| 121 |
+
- [ ] Fix applied to `src/app.py`
|
| 122 |
+
- [ ] Unit test added for None parameter handling
|
| 123 |
+
- [ ] `make check` passes
|
| 124 |
+
- [ ] Test locally with `uv run python -m src.app`
|
| 125 |
+
- [ ] Verify example caching works without crash
|
| 126 |
+
- [ ] Deploy to HuggingFace Spaces
|
| 127 |
+
- [ ] Verify Space starts without error
|
| 128 |
+
|
| 129 |
+
## Lessons Learned
|
| 130 |
+
|
| 131 |
+
1. Always test Gradio apps with example caching enabled locally before deploying
|
| 132 |
+
2. Gradio's "partial examples" feature passes `None` for missing columns
|
| 133 |
+
3. Default parameter values (`str = ""`) are ignored when Gradio explicitly passes `None`
|
| 134 |
+
4. The Gradio warning about missing example values should be treated as an error
|
src/app.py
CHANGED
|
@@ -127,8 +127,13 @@ async def research_agent(
|
|
| 127 |
yield "Please enter a research question."
|
| 128 |
return
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
# BUG FIX: Prefer freshly-entered key, then persisted state
|
| 131 |
-
user_api_key = (
|
| 132 |
|
| 133 |
# Check available keys
|
| 134 |
has_openai = bool(os.getenv("OPENAI_API_KEY"))
|
|
|
|
| 127 |
yield "Please enter a research question."
|
| 128 |
return
|
| 129 |
|
| 130 |
+
# BUG FIX: Handle None values from Gradio example caching
|
| 131 |
+
# Gradio passes None for missing example columns, overriding defaults
|
| 132 |
+
api_key_str = api_key or ""
|
| 133 |
+
api_key_state_str = api_key_state or ""
|
| 134 |
+
|
| 135 |
# BUG FIX: Prefer freshly-entered key, then persisted state
|
| 136 |
+
user_api_key = (api_key_str.strip() or api_key_state_str.strip()) or None
|
| 137 |
|
| 138 |
# Check available keys
|
| 139 |
has_openai = bool(os.getenv("OPENAI_API_KEY"))
|
tests/unit/test_gradio_crash.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test that Gradio example caching doesn't crash with None parameters."""
|
| 2 |
+
|
| 3 |
+
from unittest.mock import MagicMock
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from src.utils.models import AgentEvent
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@pytest.mark.unit
|
| 11 |
+
@pytest.mark.asyncio
|
| 12 |
+
async def test_research_agent_handles_none_parameters():
|
| 13 |
+
"""
|
| 14 |
+
Test that research_agent handles None parameters gracefully.
|
| 15 |
+
|
| 16 |
+
This simulates Gradio's example caching behavior where missing
|
| 17 |
+
example columns are passed as None instead of using default values.
|
| 18 |
+
|
| 19 |
+
Bug: https://huggingface.co/spaces/MCP-1st-Birthday/DeepBoner crashed
|
| 20 |
+
because api_key=None and api_key_state=None caused .strip() to fail.
|
| 21 |
+
"""
|
| 22 |
+
# Mock the orchestrator to avoid real API calls
|
| 23 |
+
import src.app as app_module
|
| 24 |
+
from src.app import research_agent
|
| 25 |
+
|
| 26 |
+
mock_orchestrator = MagicMock()
|
| 27 |
+
|
| 28 |
+
async def mock_run(query):
|
| 29 |
+
yield AgentEvent(type="complete", message="Test complete", iteration=1)
|
| 30 |
+
|
| 31 |
+
mock_orchestrator.run = mock_run
|
| 32 |
+
|
| 33 |
+
original_configure = app_module.configure_orchestrator
|
| 34 |
+
app_module.configure_orchestrator = MagicMock(return_value=(mock_orchestrator, "Mock"))
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
# This should NOT raise AttributeError: 'NoneType' object has no attribute 'strip'
|
| 38 |
+
results = []
|
| 39 |
+
async for result in research_agent(
|
| 40 |
+
message="test query",
|
| 41 |
+
history=[],
|
| 42 |
+
mode="simple",
|
| 43 |
+
api_key=None, # Simulating Gradio passing None
|
| 44 |
+
api_key_state=None, # Simulating Gradio passing None
|
| 45 |
+
):
|
| 46 |
+
results.append(result)
|
| 47 |
+
|
| 48 |
+
# If we get here without AttributeError, the fix works
|
| 49 |
+
assert len(results) > 0, "Should have yielded at least one result"
|
| 50 |
+
|
| 51 |
+
finally:
|
| 52 |
+
app_module.configure_orchestrator = original_configure
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.mark.unit
|
| 56 |
+
@pytest.mark.asyncio
|
| 57 |
+
async def test_research_agent_handles_empty_string_parameters():
|
| 58 |
+
"""Test that empty strings (the expected default) also work."""
|
| 59 |
+
import src.app as app_module
|
| 60 |
+
from src.app import research_agent
|
| 61 |
+
|
| 62 |
+
mock_orchestrator = MagicMock()
|
| 63 |
+
|
| 64 |
+
async def mock_run(query):
|
| 65 |
+
yield AgentEvent(type="complete", message="Test complete", iteration=1)
|
| 66 |
+
|
| 67 |
+
mock_orchestrator.run = mock_run
|
| 68 |
+
|
| 69 |
+
original_configure = app_module.configure_orchestrator
|
| 70 |
+
app_module.configure_orchestrator = MagicMock(return_value=(mock_orchestrator, "Mock"))
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
results = []
|
| 74 |
+
async for result in research_agent(
|
| 75 |
+
message="test query",
|
| 76 |
+
history=[],
|
| 77 |
+
mode="simple",
|
| 78 |
+
api_key="", # Normal empty string
|
| 79 |
+
api_key_state="", # Normal empty string
|
| 80 |
+
):
|
| 81 |
+
results.append(result)
|
| 82 |
+
|
| 83 |
+
assert len(results) > 0
|
| 84 |
+
|
| 85 |
+
finally:
|
| 86 |
+
app_module.configure_orchestrator = original_configure
|