VibecoderMcSwaggins commited on
Commit
29a6844
·
1 Parent(s): 8f45b69

fix: handle None parameters from Gradio example caching (P0)

Browse files

Root 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 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 = (api_key.strip() or api_key_state.strip()) or None
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