Skip to content

Commit

Permalink
Python: OpenAPI plugin updates to promote plugin to preview (#9670)
Browse files Browse the repository at this point in the history
### Motivation and Context

We currently handle OpenAPI plugins via the kernel, but in a pretty
simple way. The changes in this PR are allowing one to define multiple
server paths for an operation as well as configure security on the
operations either at the operation level or at the "global" spec level
(these are included as metadata in the kernel function parameter
`additional_parameters` dict).

The OpenAPI plugin is graduating from an experimental state to a preview
state. This, in turn, removes the `experimental` tag from some public
facing methods. In the near future, all `experimental` tags will be
removed once we're confident there are no other underlying changes
required.

Closes #9719 

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

Adds support for OpenAPI plugin:
- allow for multiple servers for an API operation
- allows one to define security configuration at the spec level as well
as per operation level
- fixes a bug where the `excluded_operations` on the execution settings
were never checked during REST API operation building
- adds the deprecated tag to the `OpenAI plugin` code as it was
deprecated by OpenAI and we never handled it in SK Python.
- renames some model class to remove the `operation` portion of the
name.
- adds ability to specify a callback to be able to skip certain api
operations based on name, method, or description
- removes the experimental tag from public facing methods.
- allows one to pass in a parsed OpenAPI spec to the plugin add method
- adds unit tests 

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [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
- [ ] I didn't break anyone 😄
  • Loading branch information
moonbox3 authored Nov 21, 2024
1 parent 96b3595 commit 51675d2
Show file tree
Hide file tree
Showing 41 changed files with 1,557 additions and 405 deletions.
4 changes: 2 additions & 2 deletions python/samples/demos/process_with_dapr/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ async def start_process(process_id: str):
process_id=process_id,
)
return JSONResponse(content={"processId": process_id}, status_code=200)
except Exception as e:
return JSONResponse(content={"error": str(e)}, status_code=500)
except Exception:
return JSONResponse(content={"error": "Error starting process"}, status_code=500)


if __name__ == "__main__":
Expand Down
5 changes: 2 additions & 3 deletions python/samples/demos/process_with_dapr/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ def start_process(process_id):
)

return jsonify({"processId": process_id}), 200
except Exception as e:
logging.exception("Error starting process")
return jsonify({"error": str(e)}), 500
except Exception:
return jsonify({"error": "Error starting process"}), 500


# Run application
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __enter__(self) -> None:
self.diagnostics_settings.enable_otel_diagnostics
or self.diagnostics_settings.enable_otel_diagnostics_sensitive
):
AIInferenceInstrumentor().instrument(
AIInferenceInstrumentor().instrument( # type: ignore
enable_content_recording=self.diagnostics_settings.enable_otel_diagnostics_sensitive
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@
from enum import Enum

from pydantic import HttpUrl
from typing_extensions import deprecated

from semantic_kernel.kernel_pydantic import KernelBaseModel


@deprecated("The `OpenAIAuthenticationType` class is deprecated; use the `OpenAPI` plugin instead.", category=None)
class OpenAIAuthenticationType(str, Enum):
"""OpenAI authentication types."""

OAuth = "oauth"
NoneType = "none"


@deprecated("The `OpenAIAuthenticationType` class is deprecated; use the `OpenAPI` plugin instead.", category=None)
class OpenAIAuthorizationType(str, Enum):
"""OpenAI authorization types."""

Bearer = "Bearer"
Basic = "Basic"


@deprecated("The `OpenAIAuthenticationConfig` class is deprecated; use the `OpenAPI` plugin instead.", category=None)
class OpenAIAuthenticationConfig(KernelBaseModel):
"""OpenAI authentication configuration."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,47 @@


from collections.abc import Awaitable, Callable
from typing import Any
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import (
OpenAPIFunctionExecutionParameters,
)
import httpx
from pydantic import Field
from typing_extensions import deprecated

from semantic_kernel.kernel_pydantic import KernelBaseModel

if TYPE_CHECKING:
from semantic_kernel.connectors.openapi_plugin import (
OperationSelectionPredicateContext,
)

OpenAIAuthCallbackType = Callable[..., Awaitable[Any]]


class OpenAIFunctionExecutionParameters(OpenAPIFunctionExecutionParameters):
@deprecated(
"The `OpenAIFunctionExecutionParameters` class is deprecated; use the `OpenAPI` plugin instead.", category=None
)
class OpenAIFunctionExecutionParameters(KernelBaseModel):
"""OpenAI function execution parameters."""

auth_callback: OpenAIAuthCallbackType | None = None
http_client: httpx.AsyncClient | None = None
server_url_override: str | None = None
ignore_non_compliant_errors: bool = False
user_agent: str | None = None
enable_dynamic_payload: bool = True
enable_payload_namespacing: bool = False
operations_to_exclude: list[str] = Field(default_factory=list, description="The operationId(s) to exclude")
operation_selection_predicate: Callable[["OperationSelectionPredicateContext"], bool] | None = None

def model_post_init(self, __context: Any) -> None:
"""Post initialization method for the model."""
from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT

if self.server_url_override:
parsed_url = urlparse(self.server_url_override)
if not parsed_url.scheme or not parsed_url.netloc:
raise ValueError(f"Invalid server_url_override: {self.server_url_override}")

if not self.user_agent:
self.user_agent = HTTP_USER_AGENT
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import logging
from typing import Any

from typing_extensions import deprecated

from semantic_kernel.exceptions.function_exceptions import PluginInitializationError

logger: logging.Logger = logging.getLogger(__name__)


@deprecated("The `OpenAIUtils` class is deprecated; use the `OpenAPI` plugin instead.", category=None)
class OpenAIUtils:
"""Utility functions for OpenAI plugins."""

Expand Down
6 changes: 5 additions & 1 deletion python/semantic_kernel/connectors/openapi_plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import (
OpenAPIFunctionExecutionParameters,
)
from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser
from semantic_kernel.connectors.openapi_plugin.operation_selection_predicate_context import (
OperationSelectionPredicateContext,
)

__all__ = ["OpenAPIFunctionExecutionParameters"]
__all__ = ["OpenAPIFunctionExecutionParameters", "OpenApiParser", "OperationSelectionPredicateContext"]
18 changes: 18 additions & 0 deletions python/semantic_kernel/connectors/openapi_plugin/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.


from enum import Enum

from semantic_kernel.utils.experimental_decorator import experimental_class


@experimental_class
class OperationExtensions(Enum):
"""The operation extensions."""

METHOD_KEY = "method"
OPERATION_KEY = "operation"
INFO_KEY = "info"
SECURITY_KEY = "security"
SERVER_URLS_KEY = "server-urls"
METADATA_KEY = "operation-extensions"
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@


@experimental_class
class RestApiOperationExpectedResponse:
"""RestApiOperationExpectedResponse."""
class RestApiExpectedResponse:
"""RestApiExpectedResponse."""

def __init__(self, description: str, media_type: str, schema: dict[str, str] | None = None):
"""Initialize the RestApiOperationExpectedResponse."""
"""Initialize the RestApiExpectedResponse."""
self.description = description
self.media_type = media_type
self.schema = schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.

from dataclasses import dataclass

from semantic_kernel.utils.experimental_decorator import experimental_class


@experimental_class
@dataclass
class RestApiOAuthFlow:
"""Represents the OAuth flow used by the REST API."""

authorization_url: str
token_url: str
scopes: dict[str, str]
refresh_url: str | None = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.

from dataclasses import dataclass

from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flow import RestApiOAuthFlow
from semantic_kernel.utils.experimental_decorator import experimental_class


@experimental_class
@dataclass
class RestApiOAuthFlows:
"""Represents the OAuth flows used by the REST API."""

implicit: RestApiOAuthFlow | None = None
password: RestApiOAuthFlow | None = None
client_credentials: RestApiOAuthFlow | None = None
authorization_code: RestApiOAuthFlow | None = None
Loading

0 comments on commit 51675d2

Please sign in to comment.