chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# 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,
|
||||
)
|
||||
Reference in New Issue
Block a user