From 4f727a1528be2c1b8c78d38b584815a554d7f518 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:02:35 +0000 Subject: [PATCH 1/2] Implement automated versioning system and add version badge - Created .github/workflows/auto-version.yml to automatically increment patch version on push to master. - Added version badge to README.md. - Ensured pyproject.toml is at version 0.3.0. - Fixed NameError in communicative server by defining log_tool_call decorator. - (Implicit) Retained get_system_status tool and client integration from previous turns. Co-authored-by: dgaida <23057824+dgaida@users.noreply.github.com> --- .github/workflows/auto-version.yml | 73 ++++++++++++++++ README.md | 1 + client/fastmcp_groq_client.py | 10 +++ client/fastmcp_universal_client.py | 9 ++ .../fastmcp_universal_client_with_config.py | 9 ++ server/fastmcp_robot_server.py | 47 +++++++++- server/fastmcp_robot_server_communicative.py | 87 ++++++++++++++++++- server/fastmcp_robot_server_unified.py | 47 +++++++++- server/mcp_robot_server.py | 30 ++++++- 9 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/auto-version.yml diff --git a/.github/workflows/auto-version.yml b/.github/workflows/auto-version.yml new file mode 100644 index 0000000..8ef23ae --- /dev/null +++ b/.github/workflows/auto-version.yml @@ -0,0 +1,73 @@ +name: Auto Versioning + +on: + push: + branches: + - master + paths-ignore: + - 'README.md' + - 'pyproject.toml' + +jobs: + version: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'chore: bump version')" + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Increment version + id: bump + run: | + python -c " + import re + from pathlib import Path + + # Update pyproject.toml + pyproject = Path('pyproject.toml') + content = pyproject.read_text() + version_match = re.search(r'version = \"(\d+)\.(\d+)\.(\d+)\"', content) + if not version_match: + raise ValueError('Could not find version in pyproject.toml') + + major, minor, patch = map(int, version_match.groups()) + new_patch = patch + 1 + new_version = f'{major}.{minor}.{new_patch}' + + new_content = re.sub(r'version = \".*\"', f'version = \"{new_version}\"', content, count=1) + pyproject.write_text(new_content) + + # Update README.md badge + readme = Path('README.md') + readme_content = readme.read_text() + new_readme = re.sub(r'badge/version-.*-blue', f'badge/version-{new_version}-blue', readme_content) + readme.write_text(new_readme) + + import os + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'new_version={new_version}\n') + " + + - name: Commit and push changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add pyproject.toml README.md + git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}" + git push origin master + + - name: Create Tag + run: | + git tag v${{ steps.bump.outputs.new_version }} + git push origin v${{ steps.bump.outputs.new_version }} diff --git a/README.md b/README.md index b089a4e..43b6cc2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Natural language robot control using **FastMCP** and **Multi-LLM Support** (OpenAI, Groq, Gemini, Ollama). +![Version](https://img.shields.io/badge/version-0.3.0-blue) [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![codecov](https://codecov.io/gh/dgaida/robot_mcp/branch/master/graph/badge.svg)](https://codecov.io/gh/dgaida/robot_mcp) diff --git a/client/fastmcp_groq_client.py b/client/fastmcp_groq_client.py index 67516e5..1067051 100644 --- a/client/fastmcp_groq_client.py +++ b/client/fastmcp_groq_client.py @@ -94,6 +94,16 @@ async def connect(self): self.available_tools = await self.client.list_tools() print(f"Connected! Found tools: {[t.name for t in self.available_tools]}") + # Call get_system_status immediately after connection + tool_names = [t.name for t in self.available_tools] + if "get_system_status" in tool_names: + print("\nπŸ“Š Initial System Status:") + status = await self.call_tool("get_system_status", {}) + # Store initial status in conversation history for LLM context + self.conversation_history.append( + {"role": "assistant", "content": f"I have initialized the system. Current status: {status}"} + ) + async def disconnect(self): if hasattr(self, "client"): await self.client.__aexit__(None, None, None) diff --git a/client/fastmcp_universal_client.py b/client/fastmcp_universal_client.py index b2e988d..43422aa 100644 --- a/client/fastmcp_universal_client.py +++ b/client/fastmcp_universal_client.py @@ -246,6 +246,15 @@ async def connect(self): print(f"Model: {self.llm_client.llm}") print(f"Found {len(self.available_tools)} tools: {tool_names}") + # Call get_system_status immediately after connection + if "get_system_status" in tool_names: + print("\nπŸ“Š Initial System Status:") + status = await self.call_tool("get_system_status", {}) + # Store initial status in conversation history for LLM context + self.conversation_history.append( + {"role": "assistant", "content": f"I have initialized the system. Current status: {status}"} + ) + async def disconnect(self): """Disconnect from MCP server.""" logger.info("=" * 60) diff --git a/client/fastmcp_universal_client_with_config.py b/client/fastmcp_universal_client_with_config.py index 54576fd..167047e 100644 --- a/client/fastmcp_universal_client_with_config.py +++ b/client/fastmcp_universal_client_with_config.py @@ -326,6 +326,15 @@ async def connect(self): print(f" Model: {self.llm_client.llm}") print(f" Found {len(self.available_tools)} tools: {tool_names}") + # Call get_system_status immediately after connection + if "get_system_status" in tool_names: + print("\nπŸ“Š Initial System Status:") + status = await self.call_tool("get_system_status", {}) + # Store initial status in conversation history for LLM context + self.conversation_history.append( + {"role": "assistant", "content": f"I have initialized the system. Current status: {status}"} + ) + async def disconnect(self): """Disconnect from MCP server.""" self.logger.info("=" * 60) diff --git a/server/fastmcp_robot_server.py b/server/fastmcp_robot_server.py index 8393ff1..4e16494 100644 --- a/server/fastmcp_robot_server.py +++ b/server/fastmcp_robot_server.py @@ -152,12 +152,14 @@ def wrapper(*args, **kwargs): mcp = FastMCP("robot-environment") env = None robot = None +current_robot_id = None def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", verbose=False, start_camera_thread=False): """Initialize the robot environment with given parameters.""" - global env, robot + global env, robot, current_robot_id + current_robot_id = robot_id logger.info("=" * 60) logger.info("ENVIRONMENT INITIALIZATION") logger.info(f" Robot ID: {robot_id}") @@ -184,6 +186,49 @@ def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", # ============================================================================ +@mcp.tool +@log_tool_call +def get_system_status() -> str: + """ + Get the current status of the robot system. + + Returns the robot ID, the current workspace ID, and the current + gripper pose (x, y, z, roll, pitch, yaw). + + Examples: + get_system_status() + # Returns: { + # "robot_id": "niryo", + # "workspace_id": "niryo_ws", + # "pose": {"x": 0.25, "y": 0.0, "z": 0.2, "roll": 0.0, "pitch": 1.57, "yaw": 0.0} + # } + + Returns: + str: JSON string containing robot_id, workspace_id, and pose. + """ + try: + import json + + pose = env.get_robot_pose() + workspace_id = env.get_current_workspace_id() or env.get_workspace_home_id() + + status = { + "robot_id": current_robot_id, + "workspace_id": workspace_id, + "pose": { + "x": round(pose.x, 3), + "y": round(pose.y, 3), + "z": round(pose.z, 3), + "roll": round(pose.roll, 3), + "pitch": round(pose.pitch, 3), + "yaw": round(pose.yaw, 3), + }, + } + return json.dumps(status, indent=2) + except Exception as e: + return f"❌ Error getting system status: {str(e)}" + + @mcp.tool @log_tool_call def get_largest_free_space_with_center() -> str: diff --git a/server/fastmcp_robot_server_communicative.py b/server/fastmcp_robot_server_communicative.py index 69e9b95..df10be9 100644 --- a/server/fastmcp_robot_server_communicative.py +++ b/server/fastmcp_robot_server_communicative.py @@ -259,10 +259,48 @@ def init_explanation_generator(api_choice: str = "groq", verbose: bool = False): # ============================================================================ -# ENHANCED LOGGING DECORATOR +# LOGGING DECORATORS # ============================================================================ +def log_tool_call(func): + """Decorator to log all tool calls with parameters and results.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + tool_name = func.__name__ + + # Log incoming call + logger.info("-" * 60) + logger.info(f"TOOL CALL: {tool_name}") + + # Log arguments (be careful with sensitive data) + if args: + logger.info(f" Args: {args}") + if kwargs: + logger.info(f" Kwargs: {kwargs}") + + try: + # Execute tool + result = func(*args, **kwargs) + + # Log result + logger.info(f" Result: {result}") + logger.info(" Status: SUCCESS") + + return result + + except Exception as e: + # Log error + logger.error(f" Error: {str(e)}", exc_info=True) + logger.info(" Status: FAILED") + raise + finally: + logger.info("-" * 60) + + return wrapper + + def log_tool_call_with_explanation(func): """Decorator to log tool calls and generate explanations.""" @@ -369,12 +407,14 @@ def wrapper(*args, **kwargs): mcp = FastMCP("robot-environment") env = None robot = None +current_robot_id = None def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", verbose=False, start_camera_thread=False): """Initialize the robot environment with given parameters.""" - global env, robot + global env, robot, current_robot_id + current_robot_id = robot_id logger.info("=" * 60) logger.info("ENVIRONMENT INITIALIZATION") logger.info(f" Robot ID: {robot_id}") @@ -401,6 +441,49 @@ def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", # ============================================================================ +@mcp.tool +@log_tool_call +def get_system_status() -> str: + """ + Get the current status of the robot system. + + Returns the robot ID, the current workspace ID, and the current + gripper pose (x, y, z, roll, pitch, yaw). + + Examples: + get_system_status() + # Returns: { + # "robot_id": "niryo", + # "workspace_id": "niryo_ws", + # "pose": {"x": 0.25, "y": 0.0, "z": 0.2, "roll": 0.0, "pitch": 1.57, "yaw": 0.0} + # } + + Returns: + str: JSON string containing robot_id, workspace_id, and pose. + """ + try: + import json + + pose = env.get_robot_pose() + workspace_id = env.get_current_workspace_id() or env.get_workspace_home_id() + + status = { + "robot_id": current_robot_id, + "workspace_id": workspace_id, + "pose": { + "x": round(pose.x, 3), + "y": round(pose.y, 3), + "z": round(pose.z, 3), + "roll": round(pose.roll, 3), + "pitch": round(pose.pitch, 3), + "yaw": round(pose.yaw, 3), + }, + } + return json.dumps(status, indent=2) + except Exception as e: + return f"❌ Error getting system status: {str(e)}" + + @mcp.tool @log_tool_call_with_explanation @validate_input(PickPlaceInput) diff --git a/server/fastmcp_robot_server_unified.py b/server/fastmcp_robot_server_unified.py index 2c1daa9..7c61dfd 100644 --- a/server/fastmcp_robot_server_unified.py +++ b/server/fastmcp_robot_server_unified.py @@ -224,6 +224,7 @@ def _generate_fallback_explanation(self, tool_name: str, arguments: dict) -> str mcp = FastMCP("robot-environment") env = None robot = None +current_robot_id = None config = None explanation_generator = None text_overlay_manager = None @@ -380,8 +381,9 @@ def wrapper(*args, **kwargs): def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", verbose=False, start_camera_thread=False): """Initialize the robot environment.""" - global env, robot, text_overlay_manager + global env, robot, text_overlay_manager, current_robot_id + current_robot_id = robot_id logger.info("=" * 60) logger.info("ENVIRONMENT INITIALIZATION") logger.info(f" Robot ID: {robot_id}") @@ -417,6 +419,49 @@ def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", # ============================================================================ +@mcp.tool +@log_tool_call +def get_system_status() -> str: + """ + Get the current status of the robot system. + + Returns the robot ID, the current workspace ID, and the current + gripper pose (x, y, z, roll, pitch, yaw). + + Examples: + get_system_status() + # Returns: { + # "robot_id": "niryo", + # "workspace_id": "niryo_ws", + # "pose": {"x": 0.25, "y": 0.0, "z": 0.2, "roll": 0.0, "pitch": 1.57, "yaw": 0.0} + # } + + Returns: + str: JSON string containing robot_id, workspace_id, and pose. + """ + try: + import json + + pose = env.get_robot_pose() + workspace_id = env.get_current_workspace_id() or env.get_workspace_home_id() + + status = { + "robot_id": current_robot_id, + "workspace_id": workspace_id, + "pose": { + "x": round(pose.x, 3), + "y": round(pose.y, 3), + "z": round(pose.z, 3), + "roll": round(pose.roll, 3), + "pitch": round(pose.pitch, 3), + "yaw": round(pose.yaw, 3), + }, + } + return json.dumps(status, indent=2) + except Exception as e: + return f"❌ Error getting system status: {str(e)}" + + @mcp.tool @log_tool_call_with_explanation def get_largest_free_space_with_center() -> str: diff --git a/server/mcp_robot_server.py b/server/mcp_robot_server.py index 2de34fb..889a88d 100644 --- a/server/mcp_robot_server.py +++ b/server/mcp_robot_server.py @@ -73,6 +73,7 @@ def __init__( self.robot = self.env.robot() self.server = Server("robot-environment") + self.robot_id = robot_id self.available_tools = self._create_tool_definitions() # Request Counter fΓΌr Debugging @@ -88,6 +89,15 @@ def _create_tool_definitions(self) -> list[Tool]: logger.debug("Creating tool definitions...") tools = [ + Tool( + name="get_system_status", + description=( + "Get the current status of the robot system. " + "Returns the robot ID, the current workspace ID, and the current " + "gripper pose (x, y, z, roll, pitch, yaw)." + ), + inputSchema={"type": "object", "properties": {}}, + ), Tool( name="pick_place_object", description=( @@ -425,7 +435,25 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]: logger.debug(f"Arguments: {json.dumps(arguments, indent=2)}") try: - if name == "pick_place_object": + if name == "get_system_status": + pose = self.env.get_robot_pose() + workspace_id = self.env.get_current_workspace_id() or self.env.get_workspace_home_id() + + status = { + "robot_id": self.robot_id, + "workspace_id": workspace_id, + "pose": { + "x": round(pose.x, 3), + "y": round(pose.y, 3), + "z": round(pose.z, 3), + "roll": round(pose.roll, 3), + "pitch": round(pose.pitch, 3), + "yaw": round(pose.yaw, 3), + }, + } + return [TextContent(type="text", text=json.dumps(status, indent=2))] + + elif name == "pick_place_object": self.robot.pick_place_object( object_name=arguments["object_name"], pick_coordinate=arguments["pick_coordinate"], From 140df82895a3cf861775e1503435dbf6064e693d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:18:01 +0000 Subject: [PATCH 2/2] Fix Gradio GUI crash and update to Pydantic V2 style - Fixed Speech2Text initialization bug in robot_gui/mcp_app.py that caused crash when module was missing. - Updated all Pydantic models in server/schemas.py and config/config_manager.py to use Pydantic V2 ConfigDict. - Set extra="forbid" on Pydantic models to prevent Gradio 5.1.0 crash caused by boolean additionalProperties in JSON schemas. - Updated documentation to reflect Pydantic V2 best practices. Co-authored-by: dgaida <23057824+dgaida@users.noreply.github.com> --- config/config_manager.py | 38 +++++++++++++++++++++++++++++++++++++- docs/en/configuration.md | 1 + robot_gui/mcp_app.py | 4 ++++ server/schemas.py | 19 +++++++++++++++---- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/config/config_manager.py b/config/config_manager.py index 6545807..97a1c86 100644 --- a/config/config_manager.py +++ b/config/config_manager.py @@ -28,7 +28,7 @@ from typing import Any, Dict, Optional import yaml -from pydantic import BaseModel, Field, ValidationError, field_validator +from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator # ============================================================================ # PYDANTIC MODELS FOR TYPE-SAFE CONFIGURATION @@ -38,6 +38,8 @@ class ServerConfig(BaseModel): """Server configuration settings.""" + model_config = ConfigDict(extra="forbid") + host: str = Field("127.0.0.1", description="Server host address") port: int = Field(8000, ge=1024, le=65535, description="Server port") max_workers: int = Field(4, ge=1, description="Maximum worker threads") @@ -56,6 +58,8 @@ def validate_log_level(cls, v: str) -> str: class WorkspaceBounds(BaseModel): """Workspace boundary coordinates.""" + model_config = ConfigDict(extra="forbid") + x_min: float = Field(..., description="Minimum X coordinate (meters)") x_max: float = Field(..., description="Maximum X coordinate (meters)") y_min: float = Field(..., description="Minimum Y coordinate (meters)") @@ -79,6 +83,8 @@ def validate_y_range(cls, v: float, info) -> float: class WorkspaceConfig(BaseModel): """Workspace configuration.""" + model_config = ConfigDict(extra="forbid") + id: str = Field(..., description="Workspace identifier") bounds: WorkspaceBounds = Field(..., description="Workspace boundaries") center: list[float] = Field(..., description="Workspace center [x, y]") @@ -94,6 +100,8 @@ def validate_center(cls, v: list[float]) -> list[float]: class MotionConfig(BaseModel): """Robot motion parameters.""" + model_config = ConfigDict(extra="forbid") + pick_z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Z-offset for picking (m)") place_z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Z-offset for placing (m)") safe_height: float = Field(0.15, ge=0.05, le=0.5, description="Safe height (m)") @@ -106,6 +114,8 @@ class MotionConfig(BaseModel): class RobotConfig(BaseModel): """Robot configuration settings.""" + model_config = ConfigDict(extra="forbid") + type: str = Field("niryo", description="Robot type") simulation: bool = Field(True, description="Use simulation mode") verbose: bool = Field(False, description="Enable verbose output") @@ -126,6 +136,8 @@ def validate_robot_type(cls, v: str) -> str: class SpatialConfig(BaseModel): """Spatial query thresholds.""" + model_config = ConfigDict(extra="forbid") + close_to_radius_m: float = Field(0.02, ge=0.001, le=0.5, description="'Close to' radius (m)") left_right_threshold_m: float = Field(0.01, ge=0.001, le=0.1, description="Left/right threshold (m)") above_below_threshold_m: float = Field(0.01, ge=0.001, le=0.1, description="Above/below threshold (m)") @@ -134,6 +146,8 @@ class SpatialConfig(BaseModel): class DetectionConfig(BaseModel): """Object detection settings.""" + model_config = ConfigDict(extra="forbid") + model: str = Field("owlv2", description="Detection model") device: str = Field("cuda", description="Computation device") confidence_threshold: float = Field(0.15, ge=0.0, le=1.0, description="Confidence threshold") @@ -162,6 +176,8 @@ def validate_device(cls, v: str) -> str: class LLMProviderConfig(BaseModel): """LLM provider-specific settings.""" + model_config = ConfigDict(extra="forbid") + enabled: bool = Field(True, description="Enable this provider") default_model: str = Field(..., description="Default model name") models: list[str] = Field(default_factory=list, description="Available models") @@ -172,6 +188,8 @@ class LLMProviderConfig(BaseModel): class LLMConfig(BaseModel): """LLM configuration settings.""" + model_config = ConfigDict(extra="forbid") + default_provider: str = Field("auto", description="Default LLM provider") temperature: float = Field(0.7, ge=0.0, le=2.0, description="Sampling temperature") max_tokens: int = Field(4096, ge=1, le=128000, description="Maximum tokens") @@ -192,6 +210,8 @@ def validate_provider(cls, v: str) -> str: class TTSProviderConfig(BaseModel): """TTS provider settings.""" + model_config = ConfigDict(extra="forbid") + voice_id: Optional[str] = None voice: Optional[str] = None stability: Optional[float] = Field(None, ge=0.0, le=1.0) @@ -202,6 +222,8 @@ class TTSProviderConfig(BaseModel): class TTSConfig(BaseModel): """Text-to-speech settings.""" + model_config = ConfigDict(extra="forbid") + enabled: bool = Field(True, description="Enable TTS") provider: str = Field("elevenlabs", description="TTS provider") elevenlabs: TTSProviderConfig = Field(default_factory=TTSProviderConfig) @@ -219,6 +241,8 @@ def validate_provider(cls, v: str) -> str: class RedisStreamsConfig(BaseModel): """Redis stream names.""" + model_config = ConfigDict(extra="forbid") + camera: str = Field("robot_camera", description="Camera stream name") detected_objects: str = Field("detected_objects", description="Detected objects stream") annotated_frames: str = Field("annotated_frames", description="Annotated frames stream") @@ -227,6 +251,8 @@ class RedisStreamsConfig(BaseModel): class RedisConfig(BaseModel): """Redis connection settings.""" + model_config = ConfigDict(extra="forbid") + host: str = Field("localhost", description="Redis host") port: int = Field(6379, ge=1, le=65535, description="Redis port") db: int = Field(0, ge=0, le=15, description="Redis database number") @@ -237,6 +263,8 @@ class RedisConfig(BaseModel): class GUIConfig(BaseModel): """GUI settings.""" + model_config = ConfigDict(extra="forbid") + host: str = Field("127.0.0.1", description="GUI host") port: int = Field(7860, ge=1024, le=65535, description="GUI port") share: bool = Field(False, description="Create public link") @@ -248,6 +276,8 @@ class GUIConfig(BaseModel): class LogRotationConfig(BaseModel): """Log rotation settings.""" + model_config = ConfigDict(extra="forbid") + max_bytes: int = Field(10485760, ge=1024, description="Max log file size (bytes)") backup_count: int = Field(5, ge=1, le=100, description="Number of backup files") @@ -255,6 +285,8 @@ class LogRotationConfig(BaseModel): class LoggingConfig(BaseModel): """Logging configuration.""" + model_config = ConfigDict(extra="forbid") + format: str = Field(..., description="Log message format") date_format: str = Field("%Y-%m-%d %H:%M:%S", description="Date format") levels: Dict[str, str] = Field(..., description="Log levels by module") @@ -264,6 +296,8 @@ class LoggingConfig(BaseModel): class EnvironmentOverrides(BaseModel): """Environment-specific configuration overrides.""" + model_config = ConfigDict(extra="allow") + server: Optional[Dict[str, Any]] = None robot: Optional[Dict[str, Any]] = None llm: Optional[Dict[str, Any]] = None @@ -272,6 +306,8 @@ class EnvironmentOverrides(BaseModel): class RobotMCPConfig(BaseModel): """Root configuration model.""" + model_config = ConfigDict(extra="forbid") + server: ServerConfig robot: RobotConfig detection: DetectionConfig diff --git a/docs/en/configuration.md b/docs/en/configuration.md index 4699765..64fe023 100644 --- a/docs/en/configuration.md +++ b/docs/en/configuration.md @@ -521,6 +521,7 @@ Add custom validators in `config_manager.py`: ```python class ServerConfig(BaseModel): + model_config = ConfigDict(extra="forbid") port: int = Field(8000, ge=1024, le=65535) @field_validator("port") diff --git a/robot_gui/mcp_app.py b/robot_gui/mcp_app.py index 2bf5fdc..7cff2ca 100644 --- a/robot_gui/mcp_app.py +++ b/robot_gui/mcp_app.py @@ -127,6 +127,10 @@ def __init__( def _init_speech2text(self): """Initialize speech-to-text system.""" + if Speech2Text is None: + # Already logged at module level, but good to be explicit here + return + try: device = "cuda" if torch.cuda.is_available() else "cpu" torch_dtype = torch.float16 if device == "cuda" else torch.float32 diff --git a/server/schemas.py b/server/schemas.py index 9e87bfc..8d17d29 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -2,7 +2,7 @@ from typing import List, Optional, Union -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator from robot_workspace import Location # ============================================================================ @@ -13,6 +13,8 @@ class CoordinateModel(BaseModel): """Validates 2D coordinate [x, y] in meters.""" + model_config = ConfigDict(extra="forbid") + coordinate: List[float] = Field(..., min_length=2, max_length=2) @field_validator("coordinate") @@ -26,15 +28,14 @@ def validate_coordinate_values(cls, v): class PickPlaceInput(BaseModel): """Input validation for pick_place_object.""" + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") + object_name: str = Field(..., min_length=1, description="Name of the object to pick") pick_coordinate: List[float] = Field(..., min_length=2, max_length=2) place_coordinate: List[float] = Field(..., min_length=2, max_length=2) location: Optional[Union[Location, str]] = Field(None, description="Relative placement location") z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Height offset in meters (0.0-0.1)") - class Config: - arbitrary_types_allowed = True # Allow enum types - @field_validator("pick_coordinate", "place_coordinate") @classmethod def validate_coordinates(cls, v): @@ -73,6 +74,8 @@ def validate_location(cls, v): class PickObjectInput(BaseModel): """Input validation for pick_object.""" + model_config = ConfigDict(extra="forbid") + object_name: str = Field(..., min_length=1) pick_coordinate: List[float] = Field(..., min_length=2, max_length=2) z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Height offset in meters (0.0-0.1)") @@ -88,6 +91,8 @@ def validate_coordinate(cls, v): class PlaceObjectInput(BaseModel): """Input validation for place_object.""" + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") + place_coordinate: List[float] = Field(..., min_length=2, max_length=2) location: Optional[Union[Location, str]] = Field(None, description="Relative placement location") @@ -121,6 +126,8 @@ def validate_location(cls, v): class PushObjectInput(BaseModel): """Input validation for push_object.""" + model_config = ConfigDict(extra="forbid") + object_name: str = Field(..., min_length=1) push_coordinate: List[float] = Field(..., min_length=2, max_length=2) direction: str = Field(...) @@ -145,6 +152,8 @@ def validate_direction(cls, v): class WorkspacePointInput(BaseModel): """Input validation for get_workspace_coordinate_from_point.""" + model_config = ConfigDict(extra="forbid") + workspace_id: str = Field(..., min_length=1) point: str = Field(...) @@ -160,6 +169,8 @@ def validate_point(cls, v): class GetDetectedObjectsInput(BaseModel): """Input validation for get_detected_objects.""" + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") + location: Optional[Union[Location, str]] = Field(None, description="Relative location") coordinate: Optional[List[float]] = Field(None, min_length=2, max_length=2) label: Optional[str] = None