Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion packages/toolbox-core/src/toolbox_core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion packages/toolbox-core/src/toolbox_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion packages/toolbox-core/src/toolbox_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading