Skip to content

Commit 2f84e5d

Browse files
authored
Python: Add chat completion agent code interpreter sample (microsoft#12393)
### Motivation and Context <!-- 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. --> Closing microsoft#10962 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> 1. Add a sample to demonstrate how to create a chat completion agent with code interpreter capabilities using the Azure Container Apps session pool service. 2. Fix a bug that cause the session tool to throw because the upload API doesn't return the file uploaded. 3. Modify unit tests accordingly. ### 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 😄
1 parent 58a1389 commit 2f84e5d

4 files changed

Lines changed: 140 additions & 48 deletions

File tree

python/samples/concepts/plugins/azure_python_code_interpreter.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,11 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

33
import asyncio
4-
import datetime
5-
6-
from azure.core.credentials import AccessToken
7-
from azure.core.exceptions import ClientAuthenticationError
8-
from azure.identity import DefaultAzureCredential
94

105
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
116
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import SessionsPythonTool
12-
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
137
from semantic_kernel.kernel import Kernel
148

15-
auth_token: AccessToken | None = None
16-
17-
ACA_TOKEN_ENDPOINT: str = "https://acasessions.io/.default" # nosec
18-
19-
20-
async def auth_callback() -> str:
21-
"""Auth callback for the SessionsPythonTool.
22-
This is a sample auth callback that shows how to use Azure's DefaultAzureCredential
23-
to get an access token.
24-
"""
25-
global auth_token
26-
current_utc_timestamp = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
27-
28-
if not auth_token or auth_token.expires_on < current_utc_timestamp:
29-
credential = DefaultAzureCredential()
30-
31-
try:
32-
auth_token = credential.get_token(ACA_TOKEN_ENDPOINT)
33-
except ClientAuthenticationError as cae:
34-
err_messages = getattr(cae, "messages", [])
35-
raise FunctionExecutionException(
36-
f"Failed to retrieve the client auth token with messages: {' '.join(err_messages)}"
37-
) from cae
38-
39-
return auth_token.token
40-
419

4210
async def main():
4311
kernel = Kernel()
@@ -48,7 +16,7 @@ async def main():
4816
)
4917
kernel.add_service(chat_service)
5018

51-
python_code_interpreter = SessionsPythonTool(auth_callback=auth_callback)
19+
python_code_interpreter = SessionsPythonTool()
5220

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
import os
5+
6+
from semantic_kernel.agents import ChatCompletionAgent
7+
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
8+
from semantic_kernel.contents import ChatMessageContent, FunctionCallContent, FunctionResultContent
9+
from semantic_kernel.core_plugins import SessionsPythonTool
10+
11+
"""
12+
The following sample demonstrates how to create a chat completion agent with
13+
code interpreter capabilities using the Azure Container Apps session pool service.
14+
"""
15+
16+
17+
async def handle_intermediate_steps(message: ChatMessageContent) -> None:
18+
for item in message.items or []:
19+
if isinstance(item, FunctionResultContent):
20+
print(f"# Function Result:> {item.result}")
21+
elif isinstance(item, FunctionCallContent):
22+
print(f"# Function Call:> {item.name} with arguments: {item.arguments}")
23+
else:
24+
print(f"# {message.name}: {message} ")
25+
26+
27+
async def main():
28+
# 1. Create the python code interpreter tool using the SessionsPythonTool
29+
python_code_interpreter = SessionsPythonTool()
30+
31+
# 2. Create the agent
32+
agent = ChatCompletionAgent(
33+
service=AzureChatCompletion(),
34+
name="Host",
35+
instructions="Answer questions about the menu.",
36+
plugins=[python_code_interpreter],
37+
)
38+
39+
# 3. Upload a CSV file to the session
40+
csv_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "sales.csv")
41+
file_metadata = await python_code_interpreter.upload_file(local_file_path=csv_file_path)
42+
43+
# 4. Invoke the agent for a response to a task
44+
TASK = (
45+
"What's the total sum of all sales for all segments using Python? "
46+
f"Use the uploaded file {file_metadata.full_path} for reference."
47+
)
48+
print(f"# User: '{TASK}'")
49+
async for response in agent.invoke(
50+
messages=TASK,
51+
on_intermediate_message=handle_intermediate_steps,
52+
):
53+
print(f"# {response.name}: {response} ")
54+
55+
"""
56+
Sample output:
57+
# User: 'What's the total sum of all sales for all segments using Python?
58+
Use the uploaded file /mnt/data/sales.csv for reference.'
59+
# Function Call:> SessionsPythonTool-execute_code with arguments: {
60+
"code": "
61+
import pandas as pd
62+
63+
# Load the sales data
64+
file_path = '/mnt/data/sales.csv'
65+
sales_data = pd.read_csv(file_path)
66+
67+
# Calculate the total sum of sales
68+
# Assuming there's a column named 'Sales' which contains the sales amounts
69+
total_sales = sales_data['Sales'].sum()
70+
total_sales"
71+
}
72+
# Function Result:> Status:
73+
Success
74+
Result:
75+
118726350.28999999
76+
Stdout:
77+
78+
Stderr:
79+
# Host: The total sum of all sales for all segments is approximately $118,726,350.29.
80+
"""
81+
82+
83+
if __name__ == "__main__":
84+
asyncio.run(main())

python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,10 @@ async def upload_file(
255255
files = {"file": (remote_file_path, data, "application/octet-stream")}
256256
response = await self.http_client.post(url=url, files=files)
257257
response.raise_for_status()
258-
response_json = response.json()
259-
return SessionsRemoteFileMetadata.from_dict(response_json["value"][0]["properties"])
258+
uploaded_files = await self.list_files()
259+
return next(
260+
file_metadata for file_metadata in uploaded_files if file_metadata.full_path == remote_file_path
261+
)
260262
except HTTPStatusError as e:
261263
error_message = e.response.text if e.response.text else e.response.reason_phrase
262264
raise FunctionExecutionException(

python/tests/unit/core_plugins/test_sessions_python_plugin.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,9 @@ async def test_empty_call_to_container_fails_raises_exception(aca_python_session
181181
await plugin.execute_code(code="")
182182

183183

184+
@patch("httpx.AsyncClient.get")
184185
@patch("httpx.AsyncClient.post")
185-
async def test_upload_file_with_local_path(mock_post, aca_python_sessions_unit_test_env):
186+
async def test_upload_file_with_local_path(mock_post, mock_get, aca_python_sessions_unit_test_env):
186187
"""Test upload_file when providing a local file path."""
187188

188189
async def async_return(result):
@@ -196,8 +197,18 @@ async def async_return(result):
196197
patch("builtins.open", mock_open(read_data=b"file data")),
197198
):
198199
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
199-
200200
mock_response = httpx.Response(
201+
status_code=200,
202+
json={
203+
"$id": "1",
204+
"value": [],
205+
},
206+
request=mock_request,
207+
)
208+
mock_post.return_value = await async_return(mock_response)
209+
210+
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
211+
mock_get_response = httpx.Response(
201212
status_code=200,
202213
json={
203214
"$id": "1",
@@ -213,9 +224,9 @@ async def async_return(result):
213224
},
214225
],
215226
},
216-
request=mock_request,
227+
request=mock_get_request,
217228
)
218-
mock_post.return_value = await async_return(mock_response)
229+
mock_get.return_value = await async_return(mock_get_response)
219230

220231
plugin = SessionsPythonTool(
221232
auth_callback=lambda: "sample_token",
@@ -229,8 +240,9 @@ async def async_return(result):
229240
mock_post.assert_awaited_once()
230241

231242

243+
@patch("httpx.AsyncClient.get")
232244
@patch("httpx.AsyncClient.post")
233-
async def test_upload_file_with_local_path_and_no_remote(mock_post, aca_python_sessions_unit_test_env):
245+
async def test_upload_file_with_local_path_and_no_remote(mock_post, mock_get, aca_python_sessions_unit_test_env):
234246
"""Test upload_file when providing a local file path."""
235247

236248
async def async_return(result):
@@ -243,9 +255,19 @@ async def async_return(result):
243255
),
244256
patch("builtins.open", mock_open(read_data=b"file data")),
245257
):
246-
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
258+
mock_post_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
259+
mock_post_response = httpx.Response(
260+
status_code=200,
261+
json={
262+
"$id": "1",
263+
"value": [],
264+
},
265+
request=mock_post_request,
266+
)
267+
mock_post.return_value = await async_return(mock_post_response)
247268

248-
mock_response = httpx.Response(
269+
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
270+
mock_get_response = httpx.Response(
249271
status_code=200,
250272
json={
251273
"$id": "1",
@@ -261,9 +283,9 @@ async def async_return(result):
261283
},
262284
],
263285
},
264-
request=mock_request,
286+
request=mock_get_request,
265287
)
266-
mock_post.return_value = await async_return(mock_response)
288+
mock_get.return_value = await async_return(mock_get_response)
267289

268290
plugin = SessionsPythonTool(
269291
auth_callback=lambda: "sample_token",
@@ -313,9 +335,15 @@ async def async_raise_http_error(*args, **kwargs):
313335
("./file.py", "/mnt/data/input.py", "/mnt/data/input.py"),
314336
],
315337
)
338+
@patch("httpx.AsyncClient.get")
316339
@patch("httpx.AsyncClient.post")
317340
async def test_upload_file_with_buffer(
318-
mock_post, local_file_path, input_remote_file_path, expected_remote_file_path, aca_python_sessions_unit_test_env
341+
mock_post,
342+
mock_get,
343+
local_file_path,
344+
input_remote_file_path,
345+
expected_remote_file_path,
346+
aca_python_sessions_unit_test_env,
319347
):
320348
"""Test upload_file when providing file data as a BufferedReader."""
321349

@@ -330,8 +358,18 @@ async def async_return(result):
330358
patch("builtins.open", mock_open(read_data="print('hello, world~')")),
331359
):
332360
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
333-
334361
mock_response = httpx.Response(
362+
status_code=200,
363+
json={
364+
"$id": "1",
365+
"value": [],
366+
},
367+
request=mock_request,
368+
)
369+
mock_post.return_value = await async_return(mock_response)
370+
371+
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
372+
mock_get_response = httpx.Response(
335373
status_code=200,
336374
json={
337375
"$id": "1",
@@ -347,9 +385,9 @@ async def async_return(result):
347385
},
348386
],
349387
},
350-
request=mock_request,
388+
request=mock_get_request,
351389
)
352-
mock_post.return_value = await async_return(mock_response)
390+
mock_get.return_value = await async_return(mock_get_response)
353391

354392
plugin = SessionsPythonTool(auth_callback=lambda: "sample_token")
355393

0 commit comments

Comments
 (0)