Skip to content

Commit fa2073c

Browse files
shibayanmanvkaur
andauthored
Python sample for assistant skills (#33)
* Added python sample for assistant skills * Match file structure with other samples * Update to latest bindings * use bundle in python project * fix container code * update read me, add funcignore, auto create cosmos cont * define skills and todo_manager in README --------- Co-authored-by: Manvir Kaur <[email protected]>
1 parent 4c20256 commit fa2073c

9 files changed

+215
-0
lines changed

samples/assistant/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@ app.generic('AddTodo', {
5555
})
5656
```
5757

58+
Python example:
59+
60+
```py
61+
62+
skills = func.Blueprint()
63+
todo_manager = CreateTodoManager()
64+
65+
@skills.function_name("AddTodo")
66+
@skills.generic_trigger(arg_name="taskDescription", type="assistantSkillTrigger", data_type=func.DataType.STRING, functionDescription="Create a new todo task")
67+
def add_todo(taskDescription: str) -> None:
68+
if not taskDescription:
69+
raise ValueError("Task description cannot be empty")
70+
71+
logging.info(f"Adding todo: {taskDescription}")
72+
73+
todo_id = str(uuid.uuid4())[0:6]
74+
todo_manager.add_todo(TodoItem(id=todo_id, task=taskDescription))
75+
return
76+
```
77+
5878
The `AssistantSkillTrigger` attribute requires a `FunctionDescription` string value, which is text describing what the function does.
5979
This is critical for the AI assistant to be able to invoke the skill at the right time.
6080
The name of the function parameter (e.g., `taskDescription`) is also an important hint to the AI assistant about what kind of information to provide to the skill.
@@ -70,6 +90,7 @@ The sample is available in the following language stacks:
7090

7191
* [C# on the out-of-process worker](csharp-ooproc)
7292
* [nodejs](nodejs)
93+
* [python](python) - supported on host runtime version >= 4.34.0.0
7394

7495
Please refer to the [root README](../../README.md#requirements) for common prerequisites that apply to all samples.
7596

samples/assistant/python/.funcignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.git*
2+
.vscode
3+
__azurite_db*__.json
4+
__blobstorage__
5+
__queuestorage__
6+
local.settings.json
7+
test
8+
.venv
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
import azure.functions as func
3+
4+
apis = func.Blueprint()
5+
6+
7+
@apis.function_name("CreateAssistant")
8+
@apis.route(route="assistants/{assistantId}", methods=["PUT"])
9+
@apis.generic_output_binding(arg_name="requests", type="assistantCreate", data_type=func.DataType.STRING)
10+
def create_assistant(req: func.HttpRequest, requests: func.Out[str]) -> func.HttpResponse:
11+
assistantId = req.route_params.get("assistantId")
12+
instructions = """
13+
Don't make assumptions about what values to plug into functions.
14+
Ask for clarification if a user request is ambiguous.
15+
"""
16+
create_request = {
17+
"id": assistantId,
18+
"instructions": instructions
19+
}
20+
requests.set(json.dumps(create_request))
21+
response_json = {"assistantId": assistantId}
22+
return func.HttpResponse(json.dumps(response_json), status_code=202, mimetype="application/json")
23+
24+
25+
@apis.function_name("PostUserQuery")
26+
@apis.route(route="assistants/{assistantId}", methods=["POST"])
27+
@apis.generic_output_binding(arg_name="requests", type="assistantPost", data_type=func.DataType.STRING, id="{assistantId}", model="%CHAT_MODEL_DEPLOYMENT_NAME%")
28+
def post_user_query(req: func.HttpRequest, requests: func.Out[str]) -> func.HttpResponse:
29+
userMessage = req.get_body().decode("utf-8")
30+
if not userMessage:
31+
return func.HttpResponse(json.dumps({"message": "Request body is empty"}), status_code=400, mimetype="application/json")
32+
33+
requests.set(json.dumps({"userMessage": userMessage}))
34+
return func.HttpResponse(status_code=202)
35+
36+
37+
@apis.function_name("GetChatState")
38+
@apis.route(route="assistants/{assistantId}", methods=["GET"])
39+
@apis.generic_input_binding(arg_name="state", type="assistantQuery", data_type=func.DataType.STRING, id="{assistantId}", timestampUtc="{Query.timestampUTC}")
40+
def get_chat_state(req: func.HttpRequest, state: str) -> func.HttpResponse:
41+
return func.HttpResponse(state, status_code=200, mimetype="application/json")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import json
2+
import logging
3+
import uuid
4+
import azure.functions as func
5+
6+
from todo_manager import CreateTodoManager, TodoItem
7+
8+
skills = func.Blueprint()
9+
10+
todo_manager = CreateTodoManager()
11+
12+
13+
@skills.function_name("AddTodo")
14+
@skills.generic_trigger(arg_name="taskDescription", type="assistantSkillTrigger", data_type=func.DataType.STRING, functionDescription="Create a new todo task")
15+
def add_todo(taskDescription: str) -> None:
16+
if not taskDescription:
17+
raise ValueError("Task description cannot be empty")
18+
19+
logging.info(f"Adding todo: {taskDescription}")
20+
21+
todo_id = str(uuid.uuid4())[0:6]
22+
todo_manager.add_todo(TodoItem(id=todo_id, task=taskDescription))
23+
return
24+
25+
26+
@skills.function_name("GetTodos")
27+
@skills.generic_trigger(arg_name="inputIgnored", type="assistantSkillTrigger", data_type=func.DataType.STRING, functionDescription="Fetch the list of previously created todo tasks")
28+
def get_todos(inputIgnored: str) -> str:
29+
logging.info("Fetching list of todos")
30+
results = todo_manager.get_todos()
31+
return json.dumps(results)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import azure.functions as func
2+
3+
from assistant_apis import apis
4+
from assistant_skills import skills
5+
6+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
7+
8+
app.register_functions(apis)
9+
app.register_functions(skills)

samples/assistant/python/host.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "2.0",
3+
"logging": {
4+
"logLevel": {
5+
"Microsoft.Azure.WebJobs.Extensions.OpenAI": "Information"
6+
}
7+
},
8+
"extensionBundle": {
9+
"id": "Microsoft.Azure.Functions.ExtensionBundle.Preview",
10+
"version": "[4.*, 5.0.0)"
11+
},
12+
"extensions": {
13+
"openai": {
14+
"storageConnectionName": "AzureWebJobsStorage",
15+
"collectionName": "SampleChatState"
16+
}
17+
}
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"IsEncrypted": false,
3+
"Values": {
4+
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
5+
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
6+
"FUNCTIONS_WORKER_RUNTIME": "python",
7+
"CosmosDbConnectionString": "Local Cosmos DB emulator or Cosmos DB in Azure connection string",
8+
"CosmosDatabaseName": "testdb",
9+
"CosmosContainerName": "my-todos",
10+
"AZURE_OPENAI_ENDPOINT": "https://<resource-name>.openai.azure.com/",
11+
"CHAT_MODEL_DEPLOYMENT_NAME": "gpt-3.5-turbo"
12+
}
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# DO NOT include azure-functions-worker in this file
2+
# The Python Worker is managed by Azure Functions platform
3+
# Manually managing azure-functions-worker may cause unexpected issues
4+
5+
azure-functions
6+
azure-cosmos
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import abc
2+
import logging
3+
import os
4+
5+
from azure.cosmos import CosmosClient
6+
from azure.cosmos import PartitionKey
7+
8+
class TodoItem:
9+
def __init__(self, id, task):
10+
self.id = id
11+
self.task = task
12+
13+
14+
class ITodoManager(metaclass=abc.ABCMeta):
15+
@abc.abstractmethod
16+
def add_todo(self, todo: TodoItem):
17+
raise NotImplementedError()
18+
19+
@abc.abstractmethod
20+
def get_todos(self):
21+
raise NotImplementedError()
22+
23+
24+
class InMemoryTodoManager(ITodoManager):
25+
def __init__(self):
26+
self.todos = []
27+
28+
def add_todo(self, todo: TodoItem):
29+
self.todos.append(todo)
30+
31+
def get_todos(self):
32+
return [item.__dict__ for item in self.todos]
33+
34+
35+
class CosmosDbTodoManager(ITodoManager):
36+
def __init__(self, cosmos_client: CosmosClient):
37+
self.cosmos_client = cosmos_client
38+
cosmos_database_name = os.environ.get("CosmosDatabaseName")
39+
cosmos_container_name = os.environ.get("CosmosContainerName")
40+
41+
if not cosmos_database_name or not cosmos_container_name:
42+
raise ValueError("CosmosDatabaseName and CosmosContainerName must be set as environment variables or in local.settings.json")
43+
44+
self.database = self.cosmos_client.create_database_if_not_exists(cosmos_database_name)
45+
self.container = self.database.create_container_if_not_exists(id=cosmos_container_name, partition_key=PartitionKey(path="/id"))
46+
47+
def add_todo(self, todo: TodoItem):
48+
logging.info(
49+
f"Adding todo ID = {todo.id} to container '{self.container.id}'.")
50+
self.container.create_item(todo.__dict__)
51+
52+
def get_todos(self):
53+
logging.info(
54+
f"Getting all todos from container '{self.container.id}'.")
55+
results = [item for item in self.container.query_items(
56+
"SELECT * FROM c", enable_cross_partition_query=True)]
57+
logging.info(
58+
f"Found {len(results)} todos in container '{self.container.id}'.")
59+
return results
60+
61+
62+
def CreateTodoManager() -> ITodoManager:
63+
if not os.environ.get("CosmosDbConnectionString"):
64+
return InMemoryTodoManager()
65+
else:
66+
cosmos_client = CosmosClient.from_connection_string(
67+
os.environ["CosmosDbConnectionString"])
68+
return CosmosDbTodoManager(cosmos_client)

0 commit comments

Comments
 (0)