mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
### Motivation and Context This replaces passing Logger instances as arguments around with the preferred Python way of having each module define a module-wide `logger` instance at the top of the module. Fixes #3711 ### Description Removes any `log` argument and argument documentation. Replaces usage of those `log` arguments with the module-wide `logger` instance. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Eduard van Valkenburg <[email protected]>
320 lines
11 KiB
Python
320 lines
11 KiB
Python
import json
|
|
import logging
|
|
from typing import Dict, Mapping, Optional, Union
|
|
from urllib.parse import urljoin
|
|
|
|
import aiohttp
|
|
import requests
|
|
from openapi_core import Spec, unmarshal_request
|
|
from openapi_core.contrib.requests import RequestsOpenAPIRequest
|
|
from openapi_core.exceptions import OpenAPIError
|
|
from prance import ResolvingParser
|
|
|
|
from semantic_kernel import Kernel, SKContext
|
|
from semantic_kernel.connectors.ai.open_ai.const import (
|
|
USER_AGENT,
|
|
)
|
|
from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT
|
|
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
|
|
from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter
|
|
|
|
logger: logging.Logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PreparedRestApiRequest:
|
|
def __init__(
|
|
self, method: str, url: str, params=None, headers=None, request_body=None
|
|
):
|
|
self.method = method
|
|
self.url = url
|
|
self.params = params
|
|
self.headers = headers
|
|
self.request_body = request_body
|
|
|
|
def __repr__(self):
|
|
return (
|
|
"PreparedRestApiRequest("
|
|
f"method={self.method}, "
|
|
f"url={self.url}, "
|
|
f"params={self.params}, "
|
|
f"headers={self.headers}, "
|
|
f"request_body={self.request_body})"
|
|
)
|
|
|
|
def validate_request(self, spec: Spec, **kwargs):
|
|
if kwargs.get("logger"):
|
|
logger.warning(
|
|
"The `logger` parameter is deprecated. Please use the `logging` module instead."
|
|
)
|
|
request = requests.Request(
|
|
self.method,
|
|
self.url,
|
|
params=self.params,
|
|
headers=self.headers,
|
|
json=self.request_body,
|
|
)
|
|
openapi_request = RequestsOpenAPIRequest(request=request)
|
|
try:
|
|
unmarshal_request(openapi_request, spec=spec)
|
|
return True
|
|
except OpenAPIError as e:
|
|
logger.debug(f"Error validating request: {e}", exc_info=True)
|
|
return False
|
|
|
|
|
|
class RestApiOperation:
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
method: str,
|
|
server_url: str,
|
|
path: str,
|
|
summary: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
params: Optional[Mapping[str, str]] = None,
|
|
request_body: Optional[Mapping[str, str]] = None,
|
|
):
|
|
self.id = id
|
|
self.method = method
|
|
self.server_url = server_url
|
|
self.path = path
|
|
self.summary = summary
|
|
self.description = description
|
|
self.params = params
|
|
self.request_body = request_body
|
|
|
|
"""
|
|
Fills in this RestApiOperation's parameters and payload with the provided values
|
|
:param path_params: A dictionary of path parameters
|
|
:param query_params: A dictionary of query parameters
|
|
:param headers: A dictionary of headers
|
|
:param request_body: The payload of the request
|
|
:return: A PreparedRestApiRequest object
|
|
"""
|
|
|
|
def prepare_request(
|
|
self, path_params=None, query_params=None, headers=None, request_body=None
|
|
) -> PreparedRestApiRequest:
|
|
path = self.path
|
|
if path_params:
|
|
path = path.format(**path_params)
|
|
|
|
url = urljoin(self.server_url, path)
|
|
|
|
processed_query_params, processed_headers = {}, headers
|
|
for param in self.params:
|
|
param_name = param["name"]
|
|
param_schema = param["schema"]
|
|
param_default = param_schema.get("default", None)
|
|
|
|
if param["in"] == "query":
|
|
if query_params and param_name in query_params:
|
|
processed_query_params[param_name] = query_params[param_name]
|
|
elif param["schema"] and "default" in param["schema"] is not None:
|
|
processed_query_params[param_name] = param_default
|
|
elif param["in"] == "header":
|
|
if headers and param_name in headers:
|
|
processed_headers[param_name] = headers[param_name]
|
|
elif param_default is not None:
|
|
processed_headers[param_name] = param_default
|
|
elif param["in"] == "path":
|
|
if not path_params or param_name not in path_params:
|
|
raise ValueError(
|
|
f"Required path parameter {param_name} not provided"
|
|
)
|
|
|
|
processed_payload = None
|
|
if self.request_body:
|
|
if (
|
|
request_body is None
|
|
and "required" in self.request_body
|
|
and self.request_body["required"]
|
|
):
|
|
raise ValueError("Payload is required but was not provided")
|
|
content = self.request_body["content"]
|
|
content_type = list(content.keys())[0]
|
|
processed_headers["Content-Type"] = content_type
|
|
processed_payload = request_body
|
|
|
|
processed_headers[USER_AGENT] = " ".join(
|
|
(HTTP_USER_AGENT, processed_headers.get(USER_AGENT, ""))
|
|
).rstrip()
|
|
|
|
req = PreparedRestApiRequest(
|
|
method=self.method,
|
|
url=url,
|
|
params=processed_query_params,
|
|
headers=processed_headers,
|
|
request_body=processed_payload,
|
|
)
|
|
return req
|
|
|
|
def __repr__(self):
|
|
return (
|
|
"RestApiOperation("
|
|
f"id={self.id}, "
|
|
f"method={self.method}, "
|
|
f"server_url={self.server_url}, "
|
|
f"path={self.path}, "
|
|
f"params={self.params}, "
|
|
f"request_body={self.request_body}, "
|
|
f"summary={self.summary}, "
|
|
f"description={self.description})"
|
|
)
|
|
|
|
|
|
class OpenApiParser:
|
|
def __init__(self, **kwargs):
|
|
if kwargs.get("logger"):
|
|
logger.warning(
|
|
"The `logger` parameter is deprecated. Please use the `logging` module instead."
|
|
)
|
|
|
|
"""
|
|
Import an OpenAPI file.
|
|
:param openapi_file: The path to the OpenAPI file which can be local or a URL.
|
|
:return: The parsed OpenAPI file
|
|
"""
|
|
|
|
def parse(self, openapi_document):
|
|
parser = ResolvingParser(openapi_document)
|
|
return parser.specification
|
|
|
|
"""
|
|
Creates a RestApiOperation object for each path/method combination
|
|
:param parsed_document: The parsed OpenAPI document
|
|
:return: A dictionary of RestApiOperation objects keyed by operationId
|
|
"""
|
|
|
|
def create_rest_api_operations(
|
|
self, parsed_document
|
|
) -> Dict[str, RestApiOperation]:
|
|
paths = parsed_document.get("paths", {})
|
|
request_objects = {}
|
|
for path, methods in paths.items():
|
|
for method, details in methods.items():
|
|
server_url = parsed_document.get("servers", [])
|
|
server_url = server_url[0].get("url") if server_url else "/"
|
|
|
|
request_method = method.lower()
|
|
|
|
parameters = details.get("parameters", [])
|
|
operationId = details.get("operationId", path + "_" + request_method)
|
|
summary = details.get("summary", None)
|
|
description = details.get("description", None)
|
|
|
|
rest_api_operation = RestApiOperation(
|
|
id=operationId,
|
|
method=request_method,
|
|
server_url=server_url,
|
|
path=path,
|
|
params=parameters,
|
|
request_body=details.get("requestBody", None),
|
|
summary=summary,
|
|
description=description,
|
|
)
|
|
|
|
request_objects[operationId] = rest_api_operation
|
|
return request_objects
|
|
|
|
|
|
class OpenApiRunner:
|
|
def __init__(
|
|
self,
|
|
parsed_openapi_document: Mapping[str, str],
|
|
):
|
|
self.spec = Spec.from_dict(parsed_openapi_document)
|
|
|
|
async def run_operation(
|
|
self,
|
|
operation: RestApiOperation,
|
|
path_params: Optional[Dict[str, str]] = None,
|
|
query_params: Optional[Dict[str, str]] = None,
|
|
headers: Optional[Dict[str, str]] = None,
|
|
request_body: Optional[Union[str, Dict[str, str]]] = None,
|
|
) -> aiohttp.ClientResponse:
|
|
prepared_request = operation.prepare_request(
|
|
path_params=path_params,
|
|
query_params=query_params,
|
|
headers=headers,
|
|
request_body=request_body,
|
|
)
|
|
is_valid = prepared_request.validate_request(spec=self.spec)
|
|
if not is_valid:
|
|
return None
|
|
|
|
async with aiohttp.ClientSession(raise_for_status=True) as session:
|
|
async with session.request(
|
|
prepared_request.method,
|
|
prepared_request.url,
|
|
params=prepared_request.params,
|
|
headers=prepared_request.headers,
|
|
json=prepared_request.request_body,
|
|
) as response:
|
|
return await response.text()
|
|
|
|
|
|
"""
|
|
Registers a skill with the kernel that can run OpenAPI operations.
|
|
:param kernel: The kernel to register the skill with
|
|
:param skill_name: The name of the skill
|
|
:param openapi_document: The OpenAPI document to register. Can be a filename or URL
|
|
:return: A dictionary of SKFunctions keyed by operationId
|
|
"""
|
|
|
|
|
|
def register_openapi_skill(
|
|
kernel: Kernel,
|
|
skill_name: str,
|
|
openapi_document: str,
|
|
) -> Dict[str, SKFunctionBase]:
|
|
parser = OpenApiParser()
|
|
parsed_doc = parser.parse(openapi_document)
|
|
operations = parser.create_rest_api_operations(parsed_doc)
|
|
openapi_runner = OpenApiRunner(parsed_openapi_document=parsed_doc)
|
|
|
|
skill = {}
|
|
|
|
def create_run_operation_function(
|
|
runner: OpenApiRunner, operation: RestApiOperation
|
|
):
|
|
@sk_function(
|
|
description=operation.summary
|
|
if operation.summary
|
|
else operation.description,
|
|
name=operation_id,
|
|
)
|
|
@sk_function_context_parameter(
|
|
name="path_params", description="A dictionary of path parameters"
|
|
)
|
|
@sk_function_context_parameter(
|
|
name="query_params", description="A dictionary of query parameters"
|
|
)
|
|
@sk_function_context_parameter(
|
|
name="headers", description="A dictionary of headers"
|
|
)
|
|
@sk_function_context_parameter(
|
|
name="request_body", description="A dictionary of the request body"
|
|
)
|
|
async def run_openapi_operation(sk_context: SKContext) -> str:
|
|
path_params = sk_context.variables.get("path_params")
|
|
query_params = sk_context.variables.get("query_params")
|
|
headers = sk_context.variables.get("headers")
|
|
request_body = sk_context.variables.get("request_body")
|
|
|
|
response = await runner.run_operation(
|
|
operation,
|
|
path_params=json.loads(path_params) if path_params else None,
|
|
query_params=json.loads(query_params) if query_params else None,
|
|
headers=json.loads(headers) if headers else None,
|
|
request_body=json.loads(request_body) if request_body else None,
|
|
)
|
|
return response
|
|
|
|
return run_openapi_operation
|
|
|
|
for operation_id, operation in operations.items():
|
|
logger.info(f"Registering OpenAPI operation: {skill_name}.{operation_id}")
|
|
skill[operation_id] = create_run_operation_function(openapi_runner, operation)
|
|
return kernel.import_skill(skill, skill_name)
|