diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index d85dc95111..a9c01fb066 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -118,6 +118,7 @@ def __init__( agents_client: AgentsClient | None = None, agent_id: str | None = None, agent_name: str | None = None, + agent_description: str | None = None, thread_id: str | None = None, project_endpoint: str | None = None, model_deployment_name: str | None = None, @@ -135,6 +136,7 @@ def __init__( a new agent will be created (and deleted after the request). If neither agents_client nor agent_id is provided, both will be created and managed automatically. agent_name: The name to use when creating new agents. + agent_description: The description to use when creating new agents. thread_id: Default thread ID to use for conversations. Can be overridden by conversation_id property when making a request. project_endpoint: The Azure AI Project endpoint URL. @@ -215,6 +217,7 @@ def __init__( self.credential = async_credential self.agent_id = agent_id self.agent_name = agent_name + self.agent_description = agent_description self.model_id = azure_ai_settings.model_deployment_name self.thread_id = thread_id self.should_cleanup_agent = should_cleanup_agent # Track whether we should delete the agent @@ -311,6 +314,7 @@ async def _get_agent_id_or_create(self, run_options: dict[str, Any] | None = Non args: dict[str, Any] = { "model": run_options["model"], "name": agent_name, + "description": self.agent_description, } if "tools" in run_options: args["tools"] = run_options["tools"] @@ -1038,16 +1042,19 @@ def _convert_required_action_to_tool_output( return run_id, tool_outputs, tool_approvals - def _update_agent_name(self, agent_name: str | None) -> None: + def _update_agent_name_and_description(self, agent_name: str | None, description: str | None) -> None: """Update the agent name in the chat client. Args: agent_name: The new name for the agent. + description: The new description for the agent. """ # This is a no-op in the base class, but can be overridden by subclasses # to update the agent name in the client. if agent_name and not self.agent_name: self.agent_name = agent_name + if description and not self.agent_description: + self.agent_description = description def service_url(self) -> str: """Get the service URL for the chat client. diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 67bd0bae6a..ad0e3ac961 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -62,6 +62,7 @@ def __init__( project_client: AIProjectClient | None = None, agent_name: str | None = None, agent_version: str | None = None, + agent_description: str | None = None, conversation_id: str | None = None, project_endpoint: str | None = None, model_deployment_name: str | None = None, @@ -77,6 +78,7 @@ def __init__( project_client: An existing AIProjectClient to use. If not provided, one will be created. agent_name: The name to use when creating new agents or using existing agents. agent_version: The version of the agent to use. + agent_description: The description to use when creating new agents. conversation_id: Default conversation ID to use for conversations. Can be overridden by conversation_id property when making a request. project_endpoint: The Azure AI Project endpoint URL. @@ -150,6 +152,7 @@ def __init__( # Initialize instance variables self.agent_name = agent_name self.agent_version = agent_version + self.agent_description = agent_description self.use_latest_version = use_latest_version self.project_client = project_client self.credential = async_credential @@ -280,7 +283,9 @@ async def _get_agent_reference_or_create( args["instructions"] = "".join(combined_instructions) created_agent = await self.project_client.agents.create_version( - agent_name=self.agent_name, definition=PromptAgentDefinition(**args) + agent_name=self.agent_name, + definition=PromptAgentDefinition(**args), + description=self.agent_description, ) self.agent_version = created_agent.version @@ -352,16 +357,19 @@ async def initialize_client(self) -> None: """Initialize OpenAI client.""" self.client = self.project_client.get_openai_client() # type: ignore - def _update_agent_name(self, agent_name: str | None) -> None: + def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None: """Update the agent name in the chat client. Args: agent_name: The new name for the agent. + description: The new description for the agent. """ # This is a no-op in the base class, but can be overridden by subclasses # to update the agent name in the client. if agent_name and not self.agent_name: self.agent_name = agent_name + if description and not self.agent_description: + self.agent_description = description def get_mcp_tool(self, tool: HostedMCPTool) -> Any: """Get MCP tool from HostedMCPTool.""" diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index d839eca376..98c9097072 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -86,6 +86,7 @@ def create_test_azure_ai_chat_client( client.credential = None client.agent_id = agent_id client.agent_name = agent_name + client.agent_description = None client.model_id = azure_ai_settings.model_deployment_name client.thread_id = thread_id client.should_cleanup_agent = should_cleanup_agent @@ -441,34 +442,43 @@ async def test_azure_ai_chat_client_close_client_when_should_close_false(mock_ag mock_agents_client.close.assert_not_called() -def test_azure_ai_chat_client_update_agent_name_when_current_is_none(mock_agents_client: MagicMock) -> None: - """Test _update_agent_name updates name when current agent_name is None.""" +def test_azure_ai_chat_client_update_agent_name_and_description_when_current_is_none( + mock_agents_client: MagicMock, +) -> None: + """Test _update_agent_name_and_description updates name when current agent_name is None.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client) chat_client.agent_name = None # type: ignore - chat_client._update_agent_name("NewAgentName") # type: ignore + chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore assert chat_client.agent_name == "NewAgentName" + assert chat_client.agent_description == "description" -def test_azure_ai_chat_client_update_agent_name_when_current_exists(mock_agents_client: MagicMock) -> None: - """Test _update_agent_name does not update when current agent_name exists.""" +def test_azure_ai_chat_client_update_agent_name_and_description_when_current_exists( + mock_agents_client: MagicMock, +) -> None: + """Test _update_agent_name_and_description does not update when current agent_name exists.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client) chat_client.agent_name = "ExistingName" # type: ignore + chat_client.agent_description = "ExistingDescription" # type: ignore - chat_client._update_agent_name("NewAgentName") # type: ignore + chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore assert chat_client.agent_name == "ExistingName" + assert chat_client.agent_description == "ExistingDescription" -def test_azure_ai_chat_client_update_agent_name_with_none_input(mock_agents_client: MagicMock) -> None: - """Test _update_agent_name with None input.""" +def test_azure_ai_chat_client_update_agent_name_and_description_with_none_input(mock_agents_client: MagicMock) -> None: + """Test _update_agent_name_and_description with None input.""" chat_client = create_test_azure_ai_chat_client(mock_agents_client) chat_client.agent_name = None # type: ignore + chat_client.agent_description = None # type: ignore - chat_client._update_agent_name(None) # type: ignore + chat_client._update_agent_name_and_description(None, None) # type: ignore assert chat_client.agent_name is None + assert chat_client.agent_description is None async def test_azure_ai_chat_client_create_run_options_with_messages(mock_agents_client: MagicMock) -> None: diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 8a31abcf35..151ab62041 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -84,6 +84,7 @@ def create_test_azure_ai_client( client.credential = None client.agent_name = agent_name client.agent_version = agent_version + client.agent_description = None client.use_latest_version = use_latest_version client.model_id = azure_ai_settings.model_deployment_name client.conversation_id = conversation_id @@ -397,14 +398,14 @@ async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock) mock_project_client.get_openai_client.assert_called_once() -def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> None: - """Test _update_agent_name method.""" +def test_azure_ai_client_update_agent_name_and_description(mock_project_client: MagicMock) -> None: + """Test _update_agent_name_and_description method.""" client = create_test_azure_ai_client(mock_project_client) # Test updating agent name when current is None - with patch.object(client, "_update_agent_name") as mock_update: + with patch.object(client, "_update_agent_name_and_description") as mock_update: mock_update.return_value = None - client._update_agent_name("new-agent") # type: ignore + client._update_agent_name_and_description("new-agent") # type: ignore mock_update.assert_called_once_with("new-agent") # Test behavior when agent name is updated @@ -412,9 +413,9 @@ def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> No client.agent_name = "test-agent" # Manually set for the test # Test with None input - with patch.object(client, "_update_agent_name") as mock_update: + with patch.object(client, "_update_agent_name_and_description") as mock_update: mock_update.return_value = None - client._update_agent_name(None) # type: ignore + client._update_agent_name_and_description(None) # type: ignore mock_update.assert_called_once_with(None) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index b000d4d41d..1399a4c5f0 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -719,7 +719,7 @@ def __init__( additional_properties=additional_chat_options or {}, # type: ignore ) self._async_exit_stack = AsyncExitStack() - self._update_agent_name() + self._update_agent_name_and_description() async def __aenter__(self) -> "Self": """Enter the async context manager. @@ -755,15 +755,17 @@ async def __aexit__( """ await self._async_exit_stack.aclose() - def _update_agent_name(self) -> None: + def _update_agent_name_and_description(self) -> None: """Update the agent name in the chat client. Checks if the chat client supports agent name updates. The implementation should check if there is already an agent name defined, and if not set it to this value. """ - if hasattr(self.chat_client, "_update_agent_name") and callable(self.chat_client._update_agent_name): # type: ignore[reportAttributeAccessIssue, attr-defined] - self.chat_client._update_agent_name(self.name) # type: ignore[reportAttributeAccessIssue, attr-defined] + if hasattr(self.chat_client, "_update_agent_name_and_description") and callable( + self.chat_client._update_agent_name_and_description + ): # type: ignore[reportAttributeAccessIssue, attr-defined] + self.chat_client._update_agent_name_and_description(self.name, self.description) # type: ignore[reportAttributeAccessIssue, attr-defined] async def run( self, diff --git a/python/packages/core/agent_framework/azure/_assistants_client.py b/python/packages/core/agent_framework/azure/_assistants_client.py index f0b70066df..58d2dbe309 100644 --- a/python/packages/core/agent_framework/azure/_assistants_client.py +++ b/python/packages/core/agent_framework/azure/_assistants_client.py @@ -27,6 +27,7 @@ def __init__( deployment_name: str | None = None, assistant_id: str | None = None, assistant_name: str | None = None, + assistant_description: str | None = None, thread_id: str | None = None, api_key: str | None = None, endpoint: str | None = None, @@ -49,6 +50,7 @@ def __init__( assistant_id: The ID of an Azure OpenAI assistant to use. If not provided, a new assistant will be created (and deleted after the request). assistant_name: The name to use when creating new assistants. + assistant_description: The description to use when creating new assistants. thread_id: Default thread ID to use for conversations. Can be overridden by conversation_id property when making a request. If not provided, a new thread will be created (and deleted after the request). @@ -155,6 +157,7 @@ def __init__( model_id=azure_openai_settings.chat_deployment_name, assistant_id=assistant_id, assistant_name=assistant_name, + assistant_description=assistant_description, thread_id=thread_id, async_client=async_client, # type: ignore[reportArgumentType] default_headers=default_headers, diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 2a590fd30e..8d2f9826ab 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -268,9 +268,9 @@ def _get_otlp_exporters(endpoints: list[str]) -> list["LogRecordExporter | SpanE exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = [] for endpoint in endpoints: - exporters.append(OTLPLogExporter(endpoint=endpoint)) - exporters.append(OTLPSpanExporter(endpoint=endpoint)) - exporters.append(OTLPMetricExporter(endpoint=endpoint)) + exporters.append(OTLPLogExporter(endpoint=endpoint)) # type: ignore[arg-type] + exporters.append(OTLPSpanExporter(endpoint=endpoint)) # type: ignore[arg-type] + exporters.append(OTLPMetricExporter(endpoint=endpoint)) # type: ignore[arg-type] return exporters diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 6255a6b8db..0f3bb3de63 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -64,6 +64,7 @@ def __init__( model_id: str | None = None, assistant_id: str | None = None, assistant_name: str | None = None, + assistant_description: str | None = None, thread_id: str | None = None, api_key: str | Callable[[], str | Awaitable[str]] | None = None, org_id: str | None = None, @@ -82,6 +83,7 @@ def __init__( assistant_id: The ID of an OpenAI assistant to use. If not provided, a new assistant will be created (and deleted after the request). assistant_name: The name to use when creating new assistants. + assistant_description: The description to use when creating new assistants. thread_id: Default thread ID to use for conversations. Can be overridden by conversation_id property when making a request. If not provided, a new thread will be created (and deleted after the request). @@ -147,6 +149,7 @@ def __init__( ) self.assistant_id: str | None = assistant_id self.assistant_name: str | None = assistant_name + self.assistant_description: str | None = assistant_description self.thread_id: str | None = thread_id self._should_delete_assistant: bool = False @@ -220,7 +223,11 @@ async def _get_assistant_id_or_create(self) -> str: raise ServiceInitializationError("Parameter 'model_id' is required for assistant creation.") client = await self.ensure_client() - created_assistant = await client.beta.assistants.create(name=self.assistant_name, model=self.model_id) + created_assistant = await client.beta.assistants.create( + model=self.model_id, + description=self.assistant_description, + name=self.assistant_name, + ) self.assistant_id = created_assistant.id self._should_delete_assistant = True @@ -516,13 +523,16 @@ def _convert_function_results_to_tool_output( return run_id, tool_outputs - def _update_agent_name(self, agent_name: str | None) -> None: + def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None: """Update the agent name in the chat client. Args: agent_name: The new name for the agent. + description: The new description for the agent. """ # This is a no-op in the base class, but can be overridden by subclasses # to update the agent name in the client. if agent_name and not self.assistant_name: - object.__setattr__(self, "assistant_name", agent_name) + self.assistant_name = agent_name + if description and not self.assistant_description: + self.assistant_description = description diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py index 90947dd437..b9c32b14b5 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/core/tests/openai/test_openai_assistants_client.py @@ -872,36 +872,36 @@ def test_openai_assistants_client_convert_function_results_to_tool_output_mismat assert tool_outputs[0].get("tool_call_id") == "call-456" -def test_openai_assistants_client_update_agent_name(mock_async_openai: MagicMock) -> None: - """Test _update_agent_name method updates assistant_name when not already set.""" +def test_openai_assistants_client_update_agent_name_and_description(mock_async_openai: MagicMock) -> None: + """Test _update_agent_name_and_description method updates assistant_name when not already set.""" # Test updating agent name when assistant_name is None chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None) # Call the private method to update agent name - chat_client._update_agent_name("New Assistant Name") # type: ignore + chat_client._update_agent_name_and_description("New Assistant Name") # type: ignore assert chat_client.assistant_name == "New Assistant Name" -def test_openai_assistants_client_update_agent_name_existing(mock_async_openai: MagicMock) -> None: - """Test _update_agent_name method doesn't override existing assistant_name.""" +def test_openai_assistants_client_update_agent_name_and_description_existing(mock_async_openai: MagicMock) -> None: + """Test _update_agent_name_and_description method doesn't override existing assistant_name.""" # Test that existing assistant_name is not overridden chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name="Existing Assistant") # Call the private method to update agent name - chat_client._update_agent_name("New Assistant Name") # type: ignore + chat_client._update_agent_name_and_description("New Assistant Name") # type: ignore # Should keep the existing name assert chat_client.assistant_name == "Existing Assistant" -def test_openai_assistants_client_update_agent_name_none(mock_async_openai: MagicMock) -> None: - """Test _update_agent_name method with None agent_name parameter.""" +def test_openai_assistants_client_update_agent_name_and_description_none(mock_async_openai: MagicMock) -> None: + """Test _update_agent_name_and_description method with None agent_name parameter.""" # Test that None agent_name doesn't change anything chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None) # Call the private method with None - chat_client._update_agent_name(None) # type: ignore + chat_client._update_agent_name_and_description(None) # type: ignore # Should remain None assert chat_client.assistant_name is None diff --git a/python/samples/getting_started/declarative/README.md b/python/samples/getting_started/declarative/README.md index 0956c33a67..2db3d0a82f 100644 --- a/python/samples/getting_started/declarative/README.md +++ b/python/samples/getting_started/declarative/README.md @@ -47,7 +47,17 @@ Shows how to create an agent that can search and retrieve information from Micro **Key concepts**: Azure AI Foundry integration, MCP server usage, async patterns, resource management -### 3. **Azure OpenAI Responses Agent** ([`azure_openai_responses_agent.py`](./azure_openai_responses_agent.py)) +### 3. **Inline YAML Agent** ([`inline_yaml.py`](./inline_yaml.py)) + +Shows how to create an agent using an inline YAML string rather than a file. + +- Uses Azure AI Foundry v2 Client with instructions. + +**Requirements**: `pip install agent-framework-azure-ai --pre` + +**Key concepts**: Inline YAML definition. + +### 4. **Azure OpenAI Responses Agent** ([`azure_openai_responses_agent.py`](./azure_openai_responses_agent.py)) Illustrates a basic agent using Azure OpenAI with structured responses. @@ -58,7 +68,7 @@ Illustrates a basic agent using Azure OpenAI with structured responses. **Key concepts**: Azure OpenAI integration, credential management, structured outputs -### 4. **OpenAI Responses Agent** ([`openai_responses_agent.py`](./openai_responses_agent.py)) +### 5. **OpenAI Responses Agent** ([`openai_responses_agent.py`](./openai_responses_agent.py)) Demonstrates the simplest possible agent using OpenAI directly. @@ -243,6 +253,7 @@ Each sample can be run independently. Make sure you have the required environmen # Run a specific sample python get_weather_agent.py python microsoft_learn_agent.py +python inline_yaml.py python azure_openai_responses_agent.py python openai_responses_agent.py ``` diff --git a/python/samples/getting_started/declarative/get_weather_agent.py b/python/samples/getting_started/declarative/get_weather_agent.py index 3994d8ede7..4e54af2461 100644 --- a/python/samples/getting_started/declarative/get_weather_agent.py +++ b/python/samples/getting_started/declarative/get_weather_agent.py @@ -26,7 +26,7 @@ async def main(): # create the AgentFactory with a chat client and bindings agent_factory = AgentFactory( - AzureOpenAIResponsesClient(credential=AzureCliCredential()), + chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), bindings={"get_weather": get_weather}, ) # create the agent from the yaml diff --git a/python/samples/getting_started/declarative/inline_yaml.py b/python/samples/getting_started/declarative/inline_yaml.py new file mode 100644 index 0000000000..96a3bd7b18 --- /dev/null +++ b/python/samples/getting_started/declarative/inline_yaml.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft. All rights reserved. +import asyncio + +from agent_framework.declarative import AgentFactory +from azure.identity.aio import AzureCliCredential + +""" +This sample shows how to create an agent using an inline YAML string rather than a file. + +It uses a Azure AI Client so it needs the credential to be passed into the AgentFactory. + +Prerequisites: +- `pip install agent-framework-azure-ai agent-framework-declarative --pre` +- Set the following environment variables in a .env file or your environment: + - AZURE_AI_PROJECT_ENDPOINT + - AZURE_OPENAI_MODEL +""" + + +async def main(): + """Create an agent from a declarative YAML specification and run it.""" + yaml_definition = """kind: Prompt +name: DiagnosticAgent +displayName: Diagnostic Assistant +instructions: Specialized diagnostic and issue detection agent for systems with critical error protocol and automatic handoff capabilities +description: A agent that performs diagnostics on systems and can escalate issues when critical errors are detected. + +model: + id: =Env.AZURE_OPENAI_MODEL + connection: + kind: remote + endpoint: =Env.AZURE_AI_PROJECT_ENDPOINT +""" + # create the agent from the yaml + async with ( + AzureCliCredential() as credential, + AgentFactory(client_kwargs={"async_credential": credential}).create_agent_from_yaml(yaml_definition) as agent, + ): + response = await agent.run("What can you do for me?") + print("Agent response:", response.text) + + +if __name__ == "__main__": + asyncio.run(main())