From ad8014d8f1072d8230e34577318d28e6cedb628f Mon Sep 17 00:00:00 2001 From: Shane Smithrand Date: Tue, 16 Jun 2026 21:17:43 -0400 Subject: [PATCH 1/2] fix: OpenAPI Converter was not parsing examples for request parameters --- .../http/src/utcp_http/openapi_converter.py | 75 +++++++- .../http/tests/test_openapi_converter.py | 162 ++++++++++++++++++ 2 files changed, 229 insertions(+), 8 deletions(-) diff --git a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py index d53bdfd..e9ae112 100644 --- a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py +++ b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py @@ -18,7 +18,7 @@ """ import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Literal, cast import sys import uuid from urllib.parse import urlparse @@ -248,14 +248,14 @@ def _is_auth_compatible(self, openapi_auth: Optional[Auth], auth_tools: Optional # For API Key auth, check header name and location compatibility if hasattr(openapi_auth, 'var_name') and hasattr(auth_tools, 'var_name'): - openapi_var = openapi_auth.var_name.lower() if openapi_auth.var_name else "" - tools_var = auth_tools.var_name.lower() if auth_tools.var_name else "" + openapi_var = getattr(openapi_auth, 'var_name', "").lower() if getattr(openapi_auth, 'var_name', None) else "" + tools_var = getattr(auth_tools, 'var_name', "").lower() if getattr(auth_tools, 'var_name', None) else "" if openapi_var != tools_var: return False if hasattr(openapi_auth, 'location') and hasattr(auth_tools, 'location'): - if openapi_auth.location != auth_tools.location: + if getattr(openapi_auth, 'location', None) != getattr(auth_tools, 'location', None): return False return True @@ -300,6 +300,30 @@ def _resolve_ref_obj(self, obj: Any, visited: Optional[set] = None) -> Any: if isinstance(obj, dict) and "$ref" in obj: return self._resolve_ref_path(obj["$ref"], visited) return obj + + def _extract_examples(self, obj: Dict[str, Any]) -> Optional[List[Any]]: + """ + Extract examples from an OpenAPI parameter or Media Type Object (Parameter, Media Type, Schema). + + Supports both 'example' (single value) and 'examples' (map of Example Objects). + Returns a list of example values suitable for JSON Schema 'examples' keyword. + """ + examples = [] + + # Handle single 'example' field + if "example" in obj and obj["example"] is not None: + examples.append(obj["example"]) + + # Handle 'examples' map (OpenAPI 3.0+) + if "examples" in obj and isinstance(obj["examples"], dict): + for example_obj in obj["examples"].values(): + if isinstance(example_obj, dict): + # Example Object can have 'value' or 'externalValue' + if "value" in example_obj: + examples.append(example_obj["value"]) + # Note: externalValue is a URI reference, we skip it as it's not inline + + return examples if examples else None def _create_auth_from_scheme(self, scheme: Dict[str, Any], scheme_name: str) -> Optional[Auth]: """Creates an Auth object from an OpenAPI security scheme.""" @@ -417,7 +441,7 @@ def _create_tool(self, path: str, method: str, operation: Dict[str, Any], base_u call_template = HttpCallTemplate( name=self.call_template_name, - http_method=method.upper(), + http_method=cast(Literal["GET", "POST", "PUT", "DELETE", "PATCH"], method.upper()), url=full_url, body_field=body_field if body_field else None, header_fields=header_fields if header_fields else None, @@ -466,10 +490,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch if param.get("in") == "body": body_field = "body" json_schema = self._resolve_ref_obj(param.get("schema", {}), set()) or {} - properties[body_field] = { + + # Extract examples from body parameter + body_examples = self._extract_examples(param) + + prop = { "description": param.get("description", "Request body"), **json_schema, } + if body_examples: + prop["examples"] = body_examples + + properties[body_field] = prop if param.get("required"): required.append(body_field) continue @@ -484,10 +516,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch schema["items"] = param.get("items") if "enum" in param: schema["enum"] = param.get("enum") - properties[param_name] = { + + # Extract examples from parameter + param_examples = self._extract_examples(param) + + prop = { "description": param.get("description", ""), **schema, } + if param_examples: + prop["examples"] = param_examples + + properties[param_name] = prop if param.get("required"): required.append(param_name) @@ -497,13 +537,22 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch content = request_body.get("content", {}) json_schema = content.get("application/json", {}).get("schema") json_schema = self._resolve_ref_obj(json_schema, set()) if json_schema else None + + # Extract examples from request body media type + media_type_obj = content.get("application/json", {}) + body_examples = self._extract_examples(media_type_obj) + if json_schema: # Add a single 'body' field to represent the request body body_field = "body" - properties[body_field] = { + prop = { "description": json_schema.get("description", "Request body"), **json_schema } + if body_examples: + prop["examples"] = body_examples + + properties[body_field] = prop if json_schema.get("required"): required.append(body_field) @@ -518,14 +567,17 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema: return JsonSchema() json_schema = None + media_type_obj = None if "content" in success_response: content = success_response.get("content", {}) json_schema = content.get("application/json", {}).get("schema") + media_type_obj = content.get("application/json", {}) # Fallback to any content type if application/json missing if json_schema is None and isinstance(content, dict): for v in content.values(): if isinstance(v, dict) and "schema" in v: json_schema = v.get("schema") + media_type_obj = v break elif "schema" in success_response: # OpenAPI 2.0 json_schema = success_response.get("schema") @@ -536,6 +588,9 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema: # Resolve $ref in response schema json_schema = self._resolve_ref_obj(json_schema, set()) or {} + # Extract examples from response media type + response_examples = self._extract_examples(media_type_obj) if media_type_obj else None + schema_args = { "type": json_schema.get("type", "object"), "properties": json_schema.get("properties", {}), @@ -552,5 +607,9 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema: for attr in ["enum", "minimum", "maximum", "format"]: if attr in json_schema: schema_args[attr] = json_schema.get(attr) + + # Add examples if present + if response_examples: + schema_args["examples"] = response_examples return JsonSchema(**schema_args) diff --git a/plugins/communication_protocols/http/tests/test_openapi_converter.py b/plugins/communication_protocols/http/tests/test_openapi_converter.py index aa3f3cb..b71fd0c 100644 --- a/plugins/communication_protocols/http/tests/test_openapi_converter.py +++ b/plugins/communication_protocols/http/tests/test_openapi_converter.py @@ -56,3 +56,165 @@ async def test_openapi_converter_with_auth_tools(): # Verify auth_tools is stored assert converter.auth_tools == auth_tools + + +def test_openapi_converter_parameter_examples(): + """Test that parameter examples are correctly extracted from OpenAPI spec.""" + # Create a minimal OpenAPI spec with parameter examples + openapi_spec = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/users/{userId}": { + "get": { + "operationId": "getUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "ID of the user", + "required": True, + "schema": { + "type": "string" + }, + "example": "user123" + }, + { + "name": "includeDetails", + "in": "query", + "description": "Include detailed information", + "required": False, + "schema": { + "type": "boolean" + }, + "examples": { + "trueExample": { + "summary": "Include details", + "value": True + }, + "falseExample": { + "summary": "Exclude details", + "value": False + } + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"} + } + }, + "examples": { + "userExample": { + "summary": "Example user", + "value": { + "id": "user123", + "name": "John Doe" + } + } + } + } + } + } + } + } + }, + "/users": { + "post": { + "operationId": "createUser", + "requestBody": { + "description": "User to create", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"} + }, + "required": ["name", "email"] + }, + "examples": { + "newUser": { + "summary": "New user example", + "value": { + "name": "Jane Smith", + "email": "jane@example.com" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "User created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"}, + "email": {"type": "string"} + } + } + } + } + } + } + } + } + } + } + + converter = OpenApiConverter(openapi_spec) + manual = converter.convert() + + assert len(manual.tools) == 2 + + # Test getUser tool - path and query parameter examples + get_user_tool = next((tool for tool in manual.tools if tool.name == "getUser"), None) + assert get_user_tool is not None + + # Check path parameter example + user_id_param = get_user_tool.inputs.properties.get("userId") + assert user_id_param is not None + assert user_id_param.examples is not None + assert "user123" in user_id_param.examples + + # Check query parameter examples + include_details_param = get_user_tool.inputs.properties.get("includeDetails") + assert include_details_param is not None + assert include_details_param.examples is not None + assert True in include_details_param.examples + assert False in include_details_param.examples + + # Check response examples + assert get_user_tool.outputs.examples is not None + assert len(get_user_tool.outputs.examples) > 0 + example_value = get_user_tool.outputs.examples[0] + assert example_value["id"] == "user123" + assert example_value["name"] == "John Doe" + + # Test createUser tool - request body examples + create_user_tool = next((tool for tool in manual.tools if tool.name == "createUser"), None) + assert create_user_tool is not None + + body_param = create_user_tool.inputs.properties.get("body") + assert body_param is not None + assert body_param.examples is not None + assert len(body_param.examples) > 0 + example_value = body_param.examples[0] + assert example_value["name"] == "Jane Smith" + assert example_value["email"] == "jane@example.com" From 15878ea09a50dfc68d216e0595171293113de09f Mon Sep 17 00:00:00 2001 From: Shane Smithrand Date: Tue, 16 Jun 2026 21:39:29 -0400 Subject: [PATCH 2/2] fix: resolve in extract_examples. extract_outputs now extracts examples on json_schema --- .../http/src/utcp_http/openapi_converter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py index e9ae112..3db1898 100644 --- a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py +++ b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py @@ -317,6 +317,8 @@ def _extract_examples(self, obj: Dict[str, Any]) -> Optional[List[Any]]: # Handle 'examples' map (OpenAPI 3.0+) if "examples" in obj and isinstance(obj["examples"], dict): for example_obj in obj["examples"].values(): + if isinstance(example_obj, dict) and "$ref" in example_obj: + example_obj = self._resolve_ref_obj(example_obj, set()) or {} if isinstance(example_obj, dict): # Example Object can have 'value' or 'externalValue' if "value" in example_obj: @@ -588,8 +590,12 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema: # Resolve $ref in response schema json_schema = self._resolve_ref_obj(json_schema, set()) or {} - # Extract examples from response media type - response_examples = self._extract_examples(media_type_obj) if media_type_obj else None + # Extract examples from response media type and schema level + response_examples = list(self._extract_examples(media_type_obj) or []) if media_type_obj else [] + for ex in self._extract_examples(json_schema) or []: + if ex not in response_examples: + response_examples.append(ex) + response_examples = response_examples or None schema_args = { "type": json_schema.get("type", "object"),