Skip to content

Commit 3069f18

Browse files
authored
Merge pull request #11 from dgaida/fix-gradio-5-crash-5935744820650444043
Fix Gradio 5.x crash and modernization
2 parents b861aab + c65d06f commit 3069f18

8 files changed

Lines changed: 58 additions & 47 deletions

File tree

config/config_manager.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
class ServerConfig(BaseModel):
3939
"""Server configuration settings."""
4040

41-
model_config = ConfigDict(extra="forbid")
41+
model_config = ConfigDict(extra="ignore")
4242

4343
host: str = Field("127.0.0.1", description="Server host address")
4444
port: int = Field(8000, ge=1024, le=65535, description="Server port")
@@ -59,7 +59,7 @@ def validate_log_level(cls, v: str) -> str:
5959
class WorkspaceBounds(BaseModel):
6060
"""Workspace boundary coordinates."""
6161

62-
model_config = ConfigDict(extra="forbid")
62+
model_config = ConfigDict(extra="ignore")
6363

6464
x_min: float = Field(..., description="Minimum X coordinate (meters)")
6565
x_max: float = Field(..., description="Maximum X coordinate (meters)")
@@ -86,7 +86,7 @@ def validate_y_range(cls, v: float, info) -> float:
8686
class WorkspaceConfig(BaseModel):
8787
"""Workspace configuration."""
8888

89-
model_config = ConfigDict(extra="forbid")
89+
model_config = ConfigDict(extra="ignore")
9090

9191
id: str = Field(..., description="Workspace identifier")
9292
bounds: WorkspaceBounds = Field(..., description="Workspace boundaries")
@@ -104,7 +104,7 @@ def validate_center(cls, v: list[float]) -> list[float]:
104104
class MotionConfig(BaseModel):
105105
"""Robot motion parameters."""
106106

107-
model_config = ConfigDict(extra="forbid")
107+
model_config = ConfigDict(extra="ignore")
108108

109109
pick_z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Z-offset for picking (m)")
110110
place_z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Z-offset for placing (m)")
@@ -118,7 +118,7 @@ class MotionConfig(BaseModel):
118118
class RobotConfig(BaseModel):
119119
"""Robot configuration settings."""
120120

121-
model_config = ConfigDict(extra="forbid")
121+
model_config = ConfigDict(extra="ignore")
122122

123123
type: str = Field("niryo", description="Robot type")
124124
simulation: bool = Field(True, description="Use simulation mode")
@@ -141,7 +141,7 @@ def validate_robot_type(cls, v: str) -> str:
141141
class SpatialConfig(BaseModel):
142142
"""Spatial query thresholds."""
143143

144-
model_config = ConfigDict(extra="forbid")
144+
model_config = ConfigDict(extra="ignore")
145145

146146
close_to_radius_m: float = Field(0.02, ge=0.001, le=0.5, description="'Close to' radius (m)")
147147
left_right_threshold_m: float = Field(0.01, ge=0.001, le=0.1, description="Left/right threshold (m)")
@@ -151,7 +151,7 @@ class SpatialConfig(BaseModel):
151151
class DetectionConfig(BaseModel):
152152
"""Object detection settings."""
153153

154-
model_config = ConfigDict(extra="forbid")
154+
model_config = ConfigDict(extra="ignore")
155155

156156
model: str = Field("owlv2", description="Detection model")
157157
device: str = Field("cuda", description="Computation device")
@@ -183,7 +183,7 @@ def validate_device(cls, v: str) -> str:
183183
class LLMProviderConfig(BaseModel):
184184
"""LLM provider-specific settings."""
185185

186-
model_config = ConfigDict(extra="forbid")
186+
model_config = ConfigDict(extra="ignore")
187187

188188
enabled: bool = Field(True, description="Enable this provider")
189189
default_model: str = Field(..., description="Default model name")
@@ -195,7 +195,7 @@ class LLMProviderConfig(BaseModel):
195195
class LLMConfig(BaseModel):
196196
"""LLM configuration settings."""
197197

198-
model_config = ConfigDict(extra="forbid")
198+
model_config = ConfigDict(extra="ignore")
199199

200200
default_provider: str = Field("auto", description="Default LLM provider")
201201
temperature: float = Field(0.7, ge=0.0, le=2.0, description="Sampling temperature")
@@ -218,7 +218,7 @@ def validate_provider(cls, v: str) -> str:
218218
class TTSProviderConfig(BaseModel):
219219
"""TTS provider settings."""
220220

221-
model_config = ConfigDict(extra="forbid")
221+
model_config = ConfigDict(extra="ignore")
222222

223223
voice_id: Optional[str] = None
224224
voice: Optional[str] = None
@@ -230,7 +230,7 @@ class TTSProviderConfig(BaseModel):
230230
class TTSConfig(BaseModel):
231231
"""Text-to-speech settings."""
232232

233-
model_config = ConfigDict(extra="forbid")
233+
model_config = ConfigDict(extra="ignore")
234234

235235
enabled: bool = Field(True, description="Enable TTS")
236236
provider: str = Field("elevenlabs", description="TTS provider")
@@ -250,7 +250,7 @@ def validate_provider(cls, v: str) -> str:
250250
class RedisStreamsConfig(BaseModel):
251251
"""Redis stream names."""
252252

253-
model_config = ConfigDict(extra="forbid")
253+
model_config = ConfigDict(extra="ignore")
254254

255255
camera: str = Field("robot_camera", description="Camera stream name")
256256
detected_objects: str = Field("detected_objects", description="Detected objects stream")
@@ -260,7 +260,7 @@ class RedisStreamsConfig(BaseModel):
260260
class RedisConfig(BaseModel):
261261
"""Redis connection settings."""
262262

263-
model_config = ConfigDict(extra="forbid")
263+
model_config = ConfigDict(extra="ignore")
264264

265265
host: str = Field("localhost", description="Redis host")
266266
port: int = Field(6379, ge=1, le=65535, description="Redis port")
@@ -272,7 +272,7 @@ class RedisConfig(BaseModel):
272272
class GUIConfig(BaseModel):
273273
"""GUI settings."""
274274

275-
model_config = ConfigDict(extra="forbid")
275+
model_config = ConfigDict(extra="ignore")
276276

277277
host: str = Field("127.0.0.1", description="GUI host")
278278
port: int = Field(7860, ge=1024, le=65535, description="GUI port")
@@ -285,7 +285,7 @@ class GUIConfig(BaseModel):
285285
class LogRotationConfig(BaseModel):
286286
"""Log rotation settings."""
287287

288-
model_config = ConfigDict(extra="forbid")
288+
model_config = ConfigDict(extra="ignore")
289289

290290
max_bytes: int = Field(10485760, ge=1024, description="Max log file size (bytes)")
291291
backup_count: int = Field(5, ge=1, le=100, description="Number of backup files")
@@ -294,7 +294,7 @@ class LogRotationConfig(BaseModel):
294294
class LoggingConfig(BaseModel):
295295
"""Logging configuration."""
296296

297-
model_config = ConfigDict(extra="forbid")
297+
model_config = ConfigDict(extra="ignore")
298298

299299
format: str = Field(..., description="Log message format")
300300
date_format: str = Field("%Y-%m-%d %H:%M:%S", description="Date format")
@@ -305,17 +305,22 @@ class LoggingConfig(BaseModel):
305305
class EnvironmentOverrides(BaseModel):
306306
"""Environment-specific configuration overrides."""
307307

308-
model_config = ConfigDict(extra="allow")
308+
model_config = ConfigDict(extra="ignore")
309309

310-
server: Optional[Dict[str, Any]] = None
311-
robot: Optional[Dict[str, Any]] = None
312-
llm: Optional[Dict[str, Any]] = None
310+
server: Optional[ServerConfig] = None
311+
robot: Optional[RobotConfig] = None
312+
llm: Optional[LLMConfig] = None
313+
detection: Optional[DetectionConfig] = None
314+
tts: Optional[TTSConfig] = None
315+
redis: Optional[RedisConfig] = None
316+
gui: Optional[GUIConfig] = None
317+
logging: Optional[LoggingConfig] = None
313318

314319

315320
class RobotMCPConfig(BaseModel):
316321
"""Root configuration model."""
317322

318-
model_config = ConfigDict(extra="forbid")
323+
model_config = ConfigDict(extra="ignore")
319324

320325
server: ServerConfig
321326
robot: RobotConfig

docs/en/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ Add custom validators in `config_manager.py`:
521521

522522
```python
523523
class ServerConfig(BaseModel):
524-
model_config = ConfigDict(extra="forbid")
524+
model_config = ConfigDict(extra="ignore")
525525
port: int = Field(8000, ge=1024, le=65535)
526526
527527
@field_validator("port")

docs/en/mcp_api_reference.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -987,11 +987,12 @@ class PickPlaceInput(BaseModel):
987987
if isinstance(v, Location):
988988
return v
989989
if isinstance(v, str):
990-
valid_locations = [loc.value for loc in Location if loc
990+
valid_locations = [loc.value for loc in Location if loc.value is not None]
991+
if v.lower() not in [loc.lower() for loc in valid_locations]:
992+
raise ValueError(f"Invalid location '{v}'")
993+
return v
991994
```
992995

993-
# TODO: example above uncomplete
994-
995996
---
996997

997998
## Integration Guide

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ dependencies = [
3939
[project.optional-dependencies]
4040
# GUI dependencies
4141
gui = [
42-
"gradio>=4.0.0",
42+
"gradio>=5.0.0",
4343
"redis_robot_comm @ git+https://github.com/dgaida/redis_robot_comm.git",
4444
]
4545

requirements_gradio.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ git+https://github.com/dgaida/speech2text.git
1313
git+https://github.com/dgaida/redis_robot_comm.git
1414

1515
# GUI dependencies
16-
gradio>=4.0.0,<5.0.0
16+
gradio>=5.0.0
1717

1818
# Utility packages
1919
python-dotenv>=1.0.0

robot_gui/mcp_app.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- FastMCP client integration
1010
- Real-time object detection display
1111
12-
Compatible with Gradio 3.x, 4.x, 5.x, and 6.x
12+
Compatible with Gradio 5.x and 6.x
1313
"""
1414

1515
import asyncio
@@ -170,7 +170,7 @@ async def process_chat(self, message: str, history: list):
170170
171171
Args:
172172
message: User message
173-
history: Chat history (list of tuples)
173+
history: Chat history (list of dicts for type="messages")
174174
175175
Yields:
176176
Updated chat history
@@ -180,29 +180,31 @@ async def process_chat(self, message: str, history: list):
180180
return
181181

182182
if not self.mcp_connected:
183-
history.append((message, "⚠️ MCP server not connected. Please connect first."))
183+
history.append({"role": "user", "content": message})
184+
history.append({"role": "assistant", "content": "⚠️ MCP server not connected. Please connect first."})
184185
yield history
185186
return
186187

187-
# Add user message (initially with no response)
188-
history.append((message, ""))
188+
# Add user message and initial assistant message
189+
history.append({"role": "user", "content": message})
190+
history.append({"role": "assistant", "content": ""})
189191
yield history
190192

191193
try:
192194
# Add "thinking" indicator
193-
history[-1] = (message, "🤔 Processing...")
195+
history[-1]["content"] = "🤔 Processing..."
194196
yield history
195197

196198
# Process through MCP client
197199
response = await self.mcp_client.chat(message)
198200

199201
# Update with actual response
200-
history[-1] = (message, response)
202+
history[-1]["content"] = response
201203
yield history
202204

203205
except Exception as e:
204206
error_msg = f"❌ Error: {str(e)}"
205-
history[-1] = (message, error_msg)
207+
history[-1]["content"] = error_msg
206208
yield history
207209

208210
def record_voice(self):
@@ -331,8 +333,8 @@ def create_gradio_interface(gui: RobotMCPGUI):
331333
with gr.Row():
332334
# Left: Chat interface
333335
with gr.Column(scale=2):
334-
# Simple chatbot without optional parameters for max compatibility
335-
chatbot = gr.Chatbot(label="Robot Assistant", height=500)
336+
# Use type="messages" for modern Gradio compatibility
337+
chatbot = gr.Chatbot(label="Robot Assistant", height=500, type="messages")
336338

337339
with gr.Row():
338340
msg_input = gr.Textbox(

server/schemas.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
class CoordinateModel(BaseModel):
1919
"""Validates 2D coordinate [x, y] in meters."""
2020

21-
model_config = ConfigDict(extra="forbid")
21+
model_config = ConfigDict(extra="ignore")
2222

2323
coordinate: List[float] = Field(..., min_length=2, max_length=2)
2424

@@ -34,7 +34,7 @@ def validate_coordinate_values(cls, v):
3434
class PickPlaceInput(BaseModel):
3535
"""Input validation for pick_place_object."""
3636

37-
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
37+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
3838

3939
object_name: str = Field(..., min_length=1, description="Name of the object to pick")
4040
pick_coordinate: List[float] = Field(..., min_length=2, max_length=2)
@@ -82,7 +82,7 @@ def validate_location(cls, v):
8282
class PickObjectInput(BaseModel):
8383
"""Input validation for pick_object."""
8484

85-
model_config = ConfigDict(extra="forbid")
85+
model_config = ConfigDict(extra="ignore")
8686

8787
object_name: str = Field(..., min_length=1)
8888
pick_coordinate: List[float] = Field(..., min_length=2, max_length=2)
@@ -100,7 +100,7 @@ def validate_coordinate(cls, v):
100100
class PlaceObjectInput(BaseModel):
101101
"""Input validation for place_object."""
102102

103-
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
103+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
104104

105105
place_coordinate: List[float] = Field(..., min_length=2, max_length=2)
106106
location: Optional[Union[Location, str]] = Field(None, description="Relative placement location")
@@ -137,7 +137,7 @@ def validate_location(cls, v):
137137
class PushObjectInput(BaseModel):
138138
"""Input validation for push_object."""
139139

140-
model_config = ConfigDict(extra="forbid")
140+
model_config = ConfigDict(extra="ignore")
141141

142142
object_name: str = Field(..., min_length=1)
143143
push_coordinate: List[float] = Field(..., min_length=2, max_length=2)
@@ -165,7 +165,7 @@ def validate_direction(cls, v):
165165
class WorkspacePointInput(BaseModel):
166166
"""Input validation for get_workspace_coordinate_from_point."""
167167

168-
model_config = ConfigDict(extra="forbid")
168+
model_config = ConfigDict(extra="ignore")
169169

170170
workspace_id: str = Field(..., min_length=1)
171171
point: str = Field(...)
@@ -183,7 +183,7 @@ def validate_point(cls, v):
183183
class GetDetectedObjectsInput(BaseModel):
184184
"""Input validation for get_detected_objects."""
185185

186-
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
186+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
187187

188188
location: Optional[Union[Location, str]] = Field(None, description="Relative location")
189189
coordinate: Optional[List[float]] = Field(None, min_length=2, max_length=2)

tests/test_gui.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ async def test_process_chat_not_connected(self, gui):
9292
async for update in gui.process_chat("Hello", history):
9393
updated_history = update
9494

95-
assert len(updated_history) == 1
96-
assert "not connected" in updated_history[0][1].lower()
95+
assert len(updated_history) == 2
96+
assert updated_history[0]["role"] == "user"
97+
assert updated_history[0]["content"] == "Hello"
98+
assert "not connected" in updated_history[1]["content"].lower()
9799

98100
@pytest.mark.asyncio
99101
async def test_process_chat_success(self, gui, mock_dependencies):
@@ -110,7 +112,8 @@ async def test_process_chat_success(self, gui, mock_dependencies):
110112

111113
assert len(updates) > 0
112114
final_history = updates[-1]
113-
assert final_history[0] == ("Pick up pencil", "Robot response")
115+
assert final_history[0] == {"role": "user", "content": "Pick up pencil"}
116+
assert final_history[1] == {"role": "assistant", "content": "Robot response"}
114117
mock_client.chat.assert_called_once_with("Pick up pencil")
115118

116119
def test_record_voice_success(self, gui, mock_dependencies):

0 commit comments

Comments
 (0)