diff --git a/packages/toolbox-core/src/toolbox_core/protocol.py b/packages/toolbox-core/src/toolbox_core/protocol.py index bc4fe51f..3818010f 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.py +++ b/packages/toolbox-core/src/toolbox_core/protocol.py @@ -83,6 +83,32 @@ class ParameterSchema(BaseModel): additionalProperties: Optional[Union[bool, AdditionalPropertiesSchema]] = None default: Optional[Any] = None + def get_python_safe_field_name(self) -> str: + """ + Returns a Python-safe identifier for this parameter name. + + Some parameter names (for example HTTP header names like "X-Application-ID") + are not valid Python identifiers and cannot be used directly in function + signatures or introspection utilities. This helper converts such names into + a safe form by: + - Replacing non-alphanumeric characters with underscores. + - Prefixing with 'param_' if the result starts with a digit or is empty. + """ + name = self.name + if name.isidentifier(): + return name + + # Replace any non-alphanumeric/underscore character with underscore. + sanitized = "".join( + ch if (ch.isalnum() or ch == "_") else "_" for ch in name + ) + + # Ensure the identifier does not start with a digit and is non-empty. + if not sanitized or sanitized[0].isdigit(): + sanitized = f"param_{sanitized}" if sanitized else "param_" + + return sanitized + @property def has_default(self) -> bool: """Returns True if `default` was explicitly provided in schema input.""" @@ -119,7 +145,7 @@ def to_param(self) -> Parameter: default_value = self.default return Parameter( - self.name, + self.get_python_safe_field_name(), Parameter.POSITIONAL_OR_KEYWORD, annotation=self.__get_type(), default=default_value, diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 03e9cbc8..9ed63d6f 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -83,6 +83,12 @@ def __init__( self.__transport = transport self.__description = description self.__params = params + # Map from Python-safe field names back to the original schema names. + # This allows us to expose valid Python identifiers in function signatures + # while still sending the original parameter names to the Toolbox server. + self.__param_name_map: dict[str, str] = { + p.get_python_safe_field_name(): p.name for p in self.__params + } self.__pydantic_model = params_to_pydantic_model(name, self.__params) # Separate parameters into those without a default and those with a @@ -250,7 +256,12 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str: # The payload will only contain arguments explicitly provided by the user. # Optional arguments not provided by the user will not be in the payload. - payload = all_args.arguments + # At this point, keys are Python-safe field names. Map them back to the + # original schema names expected by the Toolbox server. + payload = OrderedDict() + for safe_name, value in all_args.arguments.items(): + original_name = self.__param_name_map.get(safe_name, safe_name) + payload[original_name] = value # Perform argument type validations using pydantic self.__pydantic_model.model_validate(payload) diff --git a/packages/toolbox-core/src/toolbox_core/utils.py b/packages/toolbox-core/src/toolbox_core/utils.py index 00c00157..deeadc40 100644 --- a/packages/toolbox-core/src/toolbox_core/utils.py +++ b/packages/toolbox-core/src/toolbox_core/utils.py @@ -127,7 +127,9 @@ def params_to_pydantic_model( if field.has_default: default_value = field.default - field_definitions[field.name] = cast( + python_safe_name = field.get_python_safe_field_name() + + field_definitions[python_safe_name] = cast( Any, ( field.to_param().annotation,