diff --git a/python/packages/main/agent_framework/openai/__init__.py b/python/packages/main/agent_framework/openai/__init__.py index 1b1a251d17..bf15a9e027 100644 --- a/python/packages/main/agent_framework/openai/__init__.py +++ b/python/packages/main/agent_framework/openai/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. +from ._assistants_client import * # noqa: F403 from ._chat_client import * # noqa: F403 from ._exceptions import * # noqa: F403 from ._responses_client import * # noqa: F403 diff --git a/python/packages/main/agent_framework/openai/_assistants_client.py b/python/packages/main/agent_framework/openai/_assistants_client.py index 4058ddcf19..91a75f979a 100644 --- a/python/packages/main/agent_framework/openai/_assistants_client.py +++ b/python/packages/main/agent_framework/openai/_assistants_client.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import json +import sys from collections.abc import AsyncIterable, Mapping, MutableMapping, MutableSequence from typing import Any, ClassVar @@ -39,6 +40,13 @@ from .._types import ( from ..exceptions import ServiceInitializationError from ._shared import OpenAIConfigBase, OpenAISettings +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + +__all__ = ["OpenAIAssistantsClient"] + @use_tool_calling class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): @@ -112,6 +120,18 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): client=async_client, ) + async def __aenter__(self) -> "Self": + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: + """Async context manager exit - clean up any assistants we created.""" + await self.close() + + async def close(self) -> None: + """Clean up any assistants we created.""" + await self._cleanup_assistant_if_needed() + async def _inner_get_response( self, *, @@ -236,7 +256,6 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): async with stream as response_stream: async for response in response_stream: if response.event == "thread.run.created": - response_id = response.data.id yield ChatResponseUpdate( contents=[], conversation_id=thread_id, @@ -245,6 +264,8 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): response_id=response_id, role=ChatRole.ASSISTANT, ) + elif response.event == "thread.run.step.created" and isinstance(response.data, RunStep): + response_id = response.data.run_id elif response.event == "thread.message.delta" and isinstance(response.data, MessageDeltaEvent): delta = response.data.delta role = ChatRole.USER if delta.role == "user" else ChatRole.ASSISTANT @@ -272,7 +293,7 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): ) elif ( response.event == "thread.run.completed" - and isinstance(response.data, RunStep) + and isinstance(response.data, Run) and response.data.usage is not None ): usage = response.data.usage @@ -334,7 +355,9 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): run_options["model"] = chat_options.ai_model_id run_options["top_p"] = chat_options.top_p run_options["temperature"] = chat_options.temperature - run_options["parallel_tool_calls"] = chat_options.allow_multiple_tool_calls + + if chat_options.allow_multiple_tool_calls is not None: + run_options["parallel_tool_calls"] = chat_options.allow_multiple_tool_calls if chat_options.tool_choice is not None: tool_definitions: list[MutableMapping[str, Any]] = [] diff --git a/python/samples/getting_started/agents/foundry/foundry_basic.py b/python/samples/getting_started/agents/foundry/foundry_basic.py index bfcbed40ba..0c6eb7f996 100644 --- a/python/samples/getting_started/agents/foundry/foundry_basic.py +++ b/python/samples/getting_started/agents/foundry/foundry_basic.py @@ -31,7 +31,7 @@ async def non_streaming_example() -> None: query = "What's the weather like in Seattle?" print(f"User: {query}") result = await agent.run(query) - print(f"Result: {result}\n") + print(f"Agent: {result}\n") async def streaming_example() -> None: diff --git a/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py b/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py new file mode 100644 index 0000000000..65b7fb0991 --- /dev/null +++ b/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from random import randint +from typing import Annotated + +from agent_framework import ChatClientAgent +from agent_framework.openai import OpenAIAssistantsClient +from pydantic import Field + + +def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." + + +async def non_streaming_example() -> None: + """Example of non-streaming response (get the complete result at once).""" + print("=== Non-streaming Response Example ===") + + # Since no assistant ID is provided, the assistant will be automatically created + # and deleted after getting a response + async with ChatClientAgent( + chat_client=OpenAIAssistantsClient(), + instructions="You are a helpful weather agent.", + tools=get_weather, + ) as agent: + query = "What's the weather like in Seattle?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + +async def streaming_example() -> None: + """Example of streaming response (get results as they are generated).""" + print("=== Streaming Response Example ===") + + # Since no assistant ID is provided, the assistant will be automatically created + # and deleted after getting a response + async with ChatClientAgent( + chat_client=OpenAIAssistantsClient(), + instructions="You are a helpful weather agent.", + tools=get_weather, + ) as agent: + query = "What's the weather like in Portland?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run_stream(query): + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + + +async def main() -> None: + print("=== Basic OpenAI Assistants Chat Client Agent Example ===") + + await non_streaming_example() + await streaming_example() + + +if __name__ == "__main__": + asyncio.run(main())