Skip to content

Commit

Permalink
Python: Add ACA Python Sessions (Code Interpreter) Core Plugin, sampl…
Browse files Browse the repository at this point in the history
…es, and tests (#6158)

### Motivation and Context

Adding a new core plugin to Semantic Kernel Python that leverages the
Azure Container Apps Python Sessions Container. This container allows
one, with the proper resource, to run Python in a safe, managed
environment.

<!-- 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

This PR introduces:
- The Python Sessions (code interpreter) plugin to execute code, upload
a file to the container, list files, and download files.
  - It includes a README.md with the steps to set up the ACA resource.
- New samples to show use as a plugin and auto function calling
- 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
- [X] I didn't break anyone 😄
  • Loading branch information
moonbox3 authored May 8, 2024
1 parent 2ae9dc7 commit 7e4faa3
Show file tree
Hide file tree
Showing 11 changed files with 951 additions and 1 deletion.
3 changes: 2 additions & 1 deletion python/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ AZCOSMOS_CONTAINER_NAME = ""
ASTRADB_APP_TOKEN=""
ASTRADB_ID=""
ASTRADB_REGION=""
ASTRADB_KEYSPACE=""
ASTRADB_KEYSPACE=""
ACA_POOL_MANAGEMENT_ENDPOINT=""
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright (c) Microsoft. All rights reserved.

import asyncio
import datetime

from azure.core.credentials import AccessToken
from azure.core.exceptions import ClientAuthenticationError
from azure.identity import DefaultAzureCredential

from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
AzureChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
SessionsPythonTool,
)
from semantic_kernel.core_plugins.time_plugin import TimePlugin
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.utils.settings import (
azure_container_apps_settings_from_dot_env_as_dict,
azure_openai_settings_from_dot_env_as_dict,
)

auth_token: AccessToken | None = None

ACA_TOKEN_ENDPOINT = "https://acasessions.io/.default"


async def auth_callback() -> str:
"""Auth callback for the SessionsPythonTool.
This is a sample auth callback that shows how to use Azure's DefaultAzureCredential
to get an access token.
"""
global auth_token
current_utc_timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp())

if not auth_token or auth_token.expires_on < current_utc_timestamp:
credential = DefaultAzureCredential()

try:
auth_token = credential.get_token(ACA_TOKEN_ENDPOINT)
except ClientAuthenticationError as cae:
err_messages = getattr(cae, "messages", [])
raise FunctionExecutionException(
f"Failed to retrieve the client auth token with messages: {' '.join(err_messages)}"
) from cae

return auth_token.token


kernel = Kernel()

service_id = "sessions-tool"
chat_service = AzureChatCompletion(
service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True)
)
kernel.add_service(chat_service)

sessions_tool = SessionsPythonTool(
**azure_container_apps_settings_from_dot_env_as_dict(),
auth_callback=auth_callback,
)

kernel.add_plugin(sessions_tool, "SessionsTool")
kernel.add_plugin(TimePlugin(), "Time")

chat_function = kernel.add_function(
prompt="{{$chat_history}}{{$user_input}}",
plugin_name="ChatBot",
function_name="Chat",
)

req_settings = AzureChatPromptExecutionSettings(service_id=service_id, tool_choice="auto")

filter = {"excluded_plugins": ["ChatBot"]}
req_settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(auto_invoke=True, filters=filter)

arguments = KernelArguments(settings=req_settings)

history = ChatHistory()


async def chat() -> bool:
try:
user_input = input("User:> ")
except KeyboardInterrupt:
print("\n\nExiting chat...")
return False
except EOFError:
print("\n\nExiting chat...")
return False

if user_input == "exit":
print("\n\nExiting chat...")
return False

arguments["chat_history"] = history
arguments["user_input"] = user_input
answer = await kernel.invoke(
function=chat_function,
arguments=arguments,
)
print(f"Mosscap:> {answer}")
history.add_user_message(user_input)
history.add_assistant_message(str(answer))
return True


async def main() -> None:
print(
"Welcome to the chat bot!\
\n Type 'exit' to exit.\
\n Try a Python code execution question to see the function calling in action (i.e. what is 1+1?)."
)
chatting = True
while chatting:
chatting = await chat()


if __name__ == "__main__":
asyncio.run(main())
70 changes: 70 additions & 0 deletions python/samples/concepts/plugins/azure_python_code_interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.

import asyncio
import datetime

from azure.core.credentials import AccessToken
from azure.core.exceptions import ClientAuthenticationError
from azure.identity import DefaultAzureCredential

from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
SessionsPythonTool,
)
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
from semantic_kernel.kernel import Kernel
from semantic_kernel.utils.settings import (
azure_container_apps_settings_from_dot_env_as_dict,
azure_openai_settings_from_dot_env_as_dict,
)

auth_token: AccessToken | None = None

ACA_TOKEN_ENDPOINT = "https://acasessions.io/.default"


async def auth_callback() -> str:
"""Auth callback for the SessionsPythonTool.
This is a sample auth callback that shows how to use Azure's DefaultAzureCredential
to get an access token.
"""
global auth_token
current_utc_timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp())

if not auth_token or auth_token.expires_on < current_utc_timestamp:
credential = DefaultAzureCredential()

try:
auth_token = credential.get_token(ACA_TOKEN_ENDPOINT)
except ClientAuthenticationError as cae:
err_messages = getattr(cae, "messages", [])
raise FunctionExecutionException(
f"Failed to retrieve the client auth token with messages: {' '.join(err_messages)}"
) from cae

return auth_token.token


async def main():
kernel = Kernel()

service_id = "python-code-interpreter"
chat_service = AzureChatCompletion(
service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True)
)
kernel.add_service(chat_service)

python_code_interpreter = SessionsPythonTool(
**azure_container_apps_settings_from_dot_env_as_dict(), auth_callback=auth_callback
)

sessions_tool = kernel.add_plugin(python_code_interpreter, "PythonCodeInterpreter")

code = "import json\n\ndef add_numbers(a, b):\n return a + b\n\nargs = '{\"a\": 1, \"b\": 1}'\nargs_dict = json.loads(args)\nprint(add_numbers(args_dict['a'], args_dict['b']))" # noqa: E501
result = await kernel.invoke(sessions_tool["execute_code"], code=code)

print(result)


if __name__ == "__main__":
asyncio.run(main())
4 changes: 4 additions & 0 deletions python/semantic_kernel/core_plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
)
from semantic_kernel.core_plugins.http_plugin import HttpPlugin
from semantic_kernel.core_plugins.math_plugin import MathPlugin
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
SessionsPythonTool,
)
from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin
from semantic_kernel.core_plugins.text_plugin import TextPlugin
from semantic_kernel.core_plugins.time_plugin import TimePlugin
Expand All @@ -17,5 +20,6 @@
"HttpPlugin",
"ConversationSummaryPlugin",
"MathPlugin",
"SessionsPythonTool",
"WebSearchEnginePlugin",
]
132 changes: 132 additions & 0 deletions python/semantic_kernel/core_plugins/sessions_python_tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Getting Started with the Sessions Python Plugin

## Authentication to ARM (management.azure.com)

For any call to ARM (management.azure.com), use the access token retrieved from the below call:

```az account get-access-token --resource <https://management.azure.com/>```

## Generate a Session Pool

a. Call the following API to generate a Session Pool:

```PUT <https://management.azure.com/subscriptions/{{SubscriptionId}}/resourceGroups/{{ResourceGroup}}/providers/Microsoft.App/sessionPools/{{SessionPoolName}}?api-version=2023-08-01-preview>```

Body properties:

- location: Azure Region
- properties:
- poolManagementType:
- Today there are two Pool Management Types supported:
- "Manual"
- In this model, the user will call generateSessions API which supports batch mode (to generate 100s of sessions in one API call, and then user is free to update/specialize the session as needed or execute code in the session)
- "Dynamic"
- In this mode, the pool management is handled by the platform. Currently, the dynamic mode is only implemented for Python code execution scenario, which has its own APIs to execute code.
- maxConcurrentSessions:
- Maximum number of active sessions allowed
- name:
- Name of the sessions pool
- dynamicPoolConfiguration: Specifies the type of sessions generated by the platform
- poolType: Type of images used for the pool
- Valid values ["JupyterPython", "FunctionsPython"]
- executionType:
- Valid values ["Timed"]
- coolDownPeriodSeconds:
- Integer representing the maximum time allowed before the platform scales down the container
- sessionPoolSecrets: Secrets associated with the Session Pool
- name: Name of the secret
- value: Secret Value

Example Generation of Session Pool:

```json
{
"location": "koreacentral",
"properties": {
"poolManagementType": "Dynamic",
"maxConcurrentSessions": 10,
"name": "{{SessionPoolName}}",
"dynamicPoolConfiguration": {
"poolType": "JupyterPython",
"executionType": "Timed",
"coolDownPeriodInSecond": 310
}
}
}
```

Curl Example:

```curl
curl -X PUT "https://management.azure.com/subscriptions/{{SubscriptionId}}/resourceGroups/{{ResourceGroup}}/providers/Microsoft.App/sessionPools/{{SessionPoolName}}?api-version=2023-08-01-preview" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d '{"location": "koreacentral","properties": { "poolManagementType": "Dynamic", "maxConcurrentSessions": 10, "name": "{{SessionPoolName}}", "dynamicPoolConfiguration": { "poolType": "JupyterPython", "executionType": "Timed", "coolDownPeriodInSecond": 310 } } }'
```

If all goes well, you should receive a 200 Status Code. The response will contain a `poolManagementEndpoint` which is required to configure the Python Plugin below.

## Configuring the Python Plugin

To successfully use the Python Plugin in Semantic Kernel, you must install the Poetry `azure` extras by running `poetry install -E azure`.

Next, in the .env file, add the `poolManagementEndpoint` value from above to the variable `ACA_POOL_MANAGEMENT_ENDPOINT`. The `poolManagementEndpoint` should look something like:

```html
https://eastus.acasessions.io/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroup}}/sessionPools/{{sessionPool}}/python/execute
```

It is possible to add the code interpreter plugin as follows:

```python
kernel = Kernel()

service_id = "azure_oai"
chat_service = AzureChatCompletion(
service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True)
)
kernel.add_service(chat_service)

python_code_interpreter = SessionsPythonTool(
**azure_container_apps_settings_from_dot_env_as_dict(), auth_callback=auth_callback
)

sessions_tool = kernel.add_plugin(python_code_interpreter, "PythonCodeInterpreter")

code = "import json\n\ndef add_numbers(a, b):\n return a + b\n\nargs = '{\"a\": 1, \"b\": 1}'\nargs_dict = json.loads(args)\nprint(add_numbers(args_dict['a'], args_dict['b']))"
result = await kernel.invoke(sessions_tool["execute_code"], code=code)

print(result)
```

Instead of hard-coding a well-formatted Python code string, you may use automatic function calling inside of SK and allow the model to form the Python and call the plugin.

The authentication callback must return a valid token for the session pool. One possible way of doing this with a `DefaultAzureCredential` is as follows:

```python
async def auth_callback() -> str:
"""Auth callback for the SessionsPythonTool.
This is a sample auth callback that shows how to use Azure's DefaultAzureCredential
to get an access token.
"""
global auth_token
current_utc_timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp())

if not auth_token or auth_token.expires_on < current_utc_timestamp:
credential = DefaultAzureCredential()

try:
auth_token = credential.get_token(ACA_TOKEN_ENDPOINT)
except ClientAuthenticationError as cae:
err_messages = getattr(cae, "messages", [])
raise FunctionExecutionException(
f"Failed to retrieve the client auth token with messages: {' '.join(err_messages)}"
) from cae

return auth_token.token
```

Currently, there are two concept examples that show this plugin in more detail:

- [Plugin example](../../../samples/concepts/plugins/azure_python_code_interpreter.py): shows the basic usage of calling the code execute function on the plugin.
- [Function Calling example](../../../samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py): shows a simple chat application that leverages the Python code interpreter plugin for function calling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.

from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
SessionsPythonTool,
)
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings import (
SessionsPythonSettings,
)

__all__ = ["SessionsPythonTool", "SessionsPythonSettings"]
Loading

0 comments on commit 7e4faa3

Please sign in to comment.