Skip to content

Python: Add ACA Python Sessions (Code Interpreter) Core Plugin, samples, and tests #6158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
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 = "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']))"
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 may be handled 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
Loading