Repository navigation
Description
Describe the bug
The OpenAPI plugin builds the query string with urlencode(segments) and passes argument values through unchanged:
semantic-kernel/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py
Lines 386 to 400 in 25bd1bb
| def build_query_string(self, arguments: dict[str, Any]) -> str: | |
| """Build the query string for the operation.""" | |
| segments = [] | |
| parameters = [p for p in self.parameters if p.location == RestApiParameterLocation.QUERY] | |
| for parameter in parameters: | |
| argument = arguments.get(parameter.name) | |
| if argument is None: | |
| if parameter.is_required: | |
| raise FunctionExecutionException( | |
| f"No argument or value is provided for the `{parameter.name}` " | |
| f"required parameter of the operation - `{self.id}`." | |
| ) | |
| continue | |
| segments.append((parameter.name, argument)) | |
| return urlencode(segments) |
Without doseq=True, urlencode calls str() on each value. So an array query parameter is sent as Python's list repr (['red', 'blue']), and a boolean is sent as True/False. APIs can't parse either. OpenAPI's default for query parameters is style: form, explode: true, which means tags=red&tags=blue and active=true. The .NET OpenAPI plugin already serializes arrays this way.
The schema advertised to the model says {"type": "array", "items": {"type": "string"}}, so a tool call from the model hits this.
Header and path parameters have the same problem, since they use str(argument) (L218, L331).
To Reproduce
semantic-kernel 1.47.0. rest_api_operation.py is the same on main (25bd1bb). This runs offline: httpx.MockTransport captures the request, and the server is an IP literal so the URL validator doesn't need DNS.
import asyncio, httpx
from semantic_kernel import Kernel
from semantic_kernel.connectors.openapi_plugin import OpenAPIFunctionExecutionParameters
spec = {
"openapi": "3.0.1", "info": {"title": "t", "version": "1"},
"servers": [{"url": "https://93.184.216.34"}],
"paths": {"/items": {"get": {
"operationId": "listItems",
"parameters": [
{"name": "tags", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}},
{"name": "active", "in": "query", "schema": {"type": "boolean"}},
],
"responses": {"200": {"description": "ok"}},
}}},
}
seen = []
def handler(request):
seen.append(str(request.url))
return httpx.Response(200, json={})
async def main():
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
kernel = Kernel()
plugin = kernel.add_plugin_from_openapi(
"api", openapi_parsed_spec=spec,
execution_settings=OpenAPIFunctionExecutionParameters(http_client=client),
)
await kernel.invoke(plugin["listItems"], tags=["red", "blue"], active=True)
print(seen[0])
asyncio.run(main())Output:
https://93.184.216.34/items?tags=%5B%27red%27%2C+%27blue%27%5D&active=True
Expected behavior
https://93.184.216.34/items?tags=red&tags=blue&active=true
(form/explode, the OpenAPI default for query parameters), with booleans written as JSON true/false.
Platform
- Language: Python
- Source: semantic-kernel 1.47.0 (pip) /
mainat 25bd1bb - AI model: n/a (no model call needed)
- IDE: n/a
- OS: Windows 11, Python 3.12
Additional context
The parser doesn't read style/explode from the spec yet (RestApiParameter is always built without them), so the smallest fix is to serialize lists as repeated keys (doseq=True) and booleans as lowercase. Happy to send a fix if that helps.
Activity
[-]Python: Bug: OpenAPI plugin sends array and boolean query parameters as Python repr (tags=['a', 'b'], active=True)[/-][+].Net: Python: Bug: OpenAPI plugin sends array and boolean query parameters as Python repr (tags=['a', 'b'], active=True)[/+]