Files
microsoft--semantic-kernel/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

214 lines
9.1 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import TYPE_CHECKING, Any
from semantic_kernel.connectors.openapi_plugin.const import OperationExtensions
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation
from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import RestApiParameter
from semantic_kernel.connectors.openapi_plugin.models.rest_api_run_options import RestApiRunOptions
from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement
from semantic_kernel.connectors.openapi_plugin.models.rest_api_uri import Uri
from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser
from semantic_kernel.connectors.openapi_plugin.openapi_runner import OpenApiRunner
from semantic_kernel.connectors.openapi_plugin.server_url_validator import ServerUrlValidationOptions
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
from semantic_kernel.schema.kernel_json_schema_builder import TYPE_MAPPING
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import (
OpenAPIFunctionExecutionParameters,
)
logger: logging.Logger = logging.getLogger(__name__)
@experimental
def create_functions_from_openapi(
plugin_name: str,
openapi_document_path: str | None = None,
openapi_parsed_spec: dict[str, Any] | None = None,
execution_settings: "OpenAPIFunctionExecutionParameters | None" = None,
) -> list[KernelFunctionFromMethod]:
"""Creates the functions from OpenAPI document.
Args:
plugin_name: The name of the plugin
openapi_document_path: The OpenAPI document path, it must be a file path to the spec (optional)
openapi_parsed_spec: The parsed OpenAPI spec (optional)
execution_settings: The execution settings
Returns:
list[KernelFunctionFromMethod]: the operations as functions
"""
parsed_doc: dict[str, Any] | Any = None
if openapi_parsed_spec is not None:
parsed_doc = openapi_parsed_spec
else:
if openapi_document_path is None:
raise FunctionExecutionException(
"Either `openapi_document_path` or `openapi_parsed_spec` must be provided."
)
# Parse the document from the given path
parser = OpenApiParser()
parsed_doc = parser.parse(
openapi_document_path,
enable_file_ref_resolution=(execution_settings.enable_file_ref_resolution if execution_settings else False),
enable_http_ref_resolution=(execution_settings.enable_http_ref_resolution if execution_settings else False),
)
if parsed_doc is None:
raise FunctionExecutionException(f"Error parsing OpenAPI document: {openapi_document_path}")
parser = OpenApiParser()
operations = parser.create_rest_api_operations(parsed_doc, execution_settings=execution_settings)
global_security_requirements = parsed_doc.get("security", [])
auth_callback = None
if execution_settings and execution_settings.auth_callback:
auth_callback = execution_settings.auth_callback
openapi_runner = OpenApiRunner(
parsed_openapi_document=parsed_doc,
auth_callback=auth_callback,
http_client=execution_settings.http_client if execution_settings else None,
enable_dynamic_payload=execution_settings.enable_dynamic_payload if execution_settings else True,
enable_payload_namespacing=execution_settings.enable_payload_namespacing if execution_settings else False,
server_url_validation_options=ServerUrlValidationOptions(
allowed_base_urls=execution_settings.server_url_validation_allowed_base_urls,
allow_private_network_access=execution_settings.allow_private_network_access,
)
if execution_settings
else None,
)
functions = []
for operation in operations.values():
try:
kernel_function = _create_function_from_operation(
openapi_runner,
operation,
plugin_name,
execution_parameters=execution_settings,
security=global_security_requirements,
)
functions.append(kernel_function)
operation.freeze()
except Exception as ex:
error_msg = f"Error while registering Rest function {plugin_name}.{operation.id}: {ex}"
logger.error(error_msg)
raise FunctionExecutionException(error_msg) from ex
return functions
@experimental
def _create_function_from_operation(
runner: OpenApiRunner,
operation: RestApiOperation,
plugin_name: str | None = None,
execution_parameters: "OpenAPIFunctionExecutionParameters | None" = None,
document_uri: str | None = None,
security: list[RestApiSecurityRequirement] | None = None,
) -> KernelFunctionFromMethod:
logger.info(f"Registering OpenAPI operation: {plugin_name}.{operation.id}")
rest_operation_params: list[RestApiParameter] = operation.get_parameters(
operation=operation,
add_payload_params_from_metadata=getattr(execution_parameters, "enable_dynamic_payload", True),
enable_payload_spacing=getattr(execution_parameters, "enable_payload_namespacing", False),
)
@kernel_function(
description=operation.summary if operation.summary else operation.description,
name=operation.id,
)
async def run_openapi_operation(
**kwargs: dict[str, Any],
) -> str:
try:
kernel_arguments = KernelArguments()
for parameter in rest_operation_params:
if parameter.alternative_name and parameter.alternative_name in kwargs:
value = kwargs[parameter.alternative_name]
if value is not None:
kernel_arguments[parameter.name] = value
continue
if parameter.name in kwargs:
value = kwargs[parameter.name]
if value is not None:
kernel_arguments[parameter.name] = value
continue
if parameter.is_required:
raise FunctionExecutionException(
f"No variable found in context to use as an argument for the "
f"`{parameter.name}` parameter of the `{plugin_name}.{operation.id}` REST function."
)
options = RestApiRunOptions(
server_url_override=(
execution_parameters.server_url_override
if execution_parameters and execution_parameters.server_url_override is not None
else None
),
api_host_url=Uri(document_uri).get_left_part() if document_uri is not None else None,
timeout=execution_parameters.timeout
if execution_parameters and execution_parameters.timeout is not None
else None,
)
return await runner.run_operation(operation, kernel_arguments, options)
except Exception as e:
logger.error(f"Error running OpenAPI operation: {operation.id}", exc_info=True)
raise FunctionExecutionException(f"Error running OpenAPI operation: {operation.id}") from e
parameters: list[KernelParameterMetadata] = [
KernelParameterMetadata(
name=p.alternative_name or p.name,
description=f"{p.description or p.name}",
default_value=p.default_value or "",
is_required=p.is_required,
type_=p.type if p.type is not None else TYPE_MAPPING.get(p.type, "object"),
schema_data=(
p.schema
if p.schema is not None and isinstance(p.schema, dict)
else {"type": f"{p.type}"}
if p.type
else None
),
)
for p in rest_operation_params
]
return_parameter = operation.get_default_return_parameter()
additional_metadata = {
OperationExtensions.METHOD_KEY.value: operation.method.upper(),
OperationExtensions.OPERATION_KEY.value: operation,
OperationExtensions.SERVER_URLS_KEY.value: (
[operation.servers[0]["url"]]
if operation.servers and len(operation.servers) > 0 and operation.servers[0]["url"]
else []
),
}
if security is not None:
additional_metadata[OperationExtensions.SECURITY_KEY.value] = security
return KernelFunctionFromMethod(
method=run_openapi_operation,
plugin_name=plugin_name,
parameters=parameters,
return_parameter=return_parameter,
additional_metadata=additional_metadata,
)