-
Notifications
You must be signed in to change notification settings - Fork 891
Python: ADR for create/get agent API #2618
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
Open
dmytrostruk
wants to merge
9
commits into
microsoft:main
Choose a base branch
from
dmytrostruk:create-agent-updates
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
00f733c
ADR for create/get agent API
dmytrostruk cd815aa
Merge branch 'main' into create-agent-updates
dmytrostruk 9095e18
Merge branch 'main' into create-agent-updates
dmytrostruk 4a97abe
Merge branch 'main' into create-agent-updates
dmytrostruk 8212dd6
Merge branch 'main' into create-agent-updates
dmytrostruk 8dfb988
Merge branch 'main' into create-agent-updates
dmytrostruk 29faa12
Merge branch 'main' into create-agent-updates
dmytrostruk 5cada6e
Updated ADR with implementation options
dmytrostruk e8907b0
Merge branch 'main' into create-agent-updates
dmytrostruk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| --- | ||
| status: proposed | ||
| contact: dmytrostruk | ||
| date: 2025-12-12 | ||
| deciders: dmytrostruk, markwallace-microsoft, eavanvalkenburg, giles17 | ||
| --- | ||
|
|
||
| # Create/Get Agent API | ||
|
|
||
| ## Context and Problem Statement | ||
|
|
||
| There is a misalignment between the create/get agent API in the .NET and Python implementations. | ||
|
|
||
| In .NET, the `CreateAIAgent` method can create either a local instance of an agent or a remote instance if the backend provider supports it. For remote agents, once the agent is created, you can retrieve an existing remote agent by using the `GetAIAgent` method. If a backend provider doesn't support remote agents, `CreateAIAgent` just initializes a new local agent instance and `GetAIAgent` is not available. There is also a `BuildAIAgent` method, which is an extension for the `ChatClientBuilder` class from `Microsoft.Extensions.AI`. It builds pipelines of `IChatClient` instances with an `IServiceProvider`. This functionality does not exist in Python, so `BuildAIAgent` is out of scope. | ||
|
|
||
| In Python, there is only one `create_agent` method, which always creates a local instance of the agent. If the backend provider supports remote agents, the remote agent is created only on the first `agent.run()` invocation. | ||
|
|
||
| Below is a short summary of different providers and their APIs in .NET: | ||
|
|
||
| | Package | Method | Behavior | Python support | | ||
| |---|---|---|---| | ||
| | Microsoft.Agents.AI | `CreateAIAgent` (based on `IChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). | | ||
| | Microsoft.Agents.AI.Anthropic | `CreateAIAgent` (based on `IBetaService` and `IAnthropicClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`AnthropicClient` inherits `BaseChatClient`, which exposes `create_agent`). | | ||
| | Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent` (based on `AIProjectClient` with `AgentReference`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). | | ||
| | Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent`/`GetAIAgentAsync` (with `Name`/`ChatClientAgentOptions`) | Fetches `AgentRecord` via HTTP, then creates a local `ChatClientAgent` instance. | No | | ||
| | Microsoft.Agents.AI.AzureAI (V2) | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AIProjectClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No | | ||
| | Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent` (based on `PersistentAgentsClient` with `PersistentAgent`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). | | ||
| | Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `PersistentAgent` via HTTP, then creates a local `ChatClientAgent` instance. | No | | ||
| | Microsoft.Agents.AI.AzureAI.Persistent (V1) | `CreateAIAgent`/`CreateAIAgentAsync` | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No | | ||
| | Microsoft.Agents.AI.OpenAI | `GetAIAgent` (based on `AssistantClient` with `Assistant`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). | | ||
| | Microsoft.Agents.AI.OpenAI | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `Assistant` via HTTP, then creates a local `ChatClientAgent` instance. | No | | ||
| | Microsoft.Agents.AI.OpenAI | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AssistantClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No | | ||
| | Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `ChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). | | ||
| | Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `OpenAIResponseClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). | | ||
|
|
||
| Another difference between Python and .NET implementation is that in .NET `CreateAIAgent`/`GetAIAgent` methods are implemented as extension methods based on underlying SDK client, like `AIProjectClient` from Azure AI or `AssistantClient` from OpenAI: | ||
|
|
||
| ```csharp | ||
| // Definition | ||
| public static ChatClientAgent CreateAIAgent( | ||
| this AIProjectClient aiProjectClient, | ||
| string name, | ||
| string model, | ||
| string instructions, | ||
| string? description = null, | ||
| IList<AITool>? tools = null, | ||
| Func<IChatClient, IChatClient>? clientFactory = null, | ||
| IServiceProvider? services = null, | ||
| CancellationToken cancellationToken = default) | ||
| { } | ||
|
|
||
| // Usage | ||
| AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); // Initialization of underlying SDK client | ||
|
|
||
| var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AgentName, model: deploymentName, instructions: AgentInstructions, tools: [tool]); // ChatClientAgent creation from underlying SDK client | ||
|
|
||
| // Alternative usage (same as extension method, just explicit syntax) | ||
| var newAgent = await AzureAIProjectChatClientExtensions.CreateAIAgentAsync( | ||
| aiProjectClient, | ||
| name: AgentName, | ||
| model: deploymentName, | ||
| instructions: AgentInstructions, | ||
| tools: [tool]); | ||
| ``` | ||
|
|
||
| Python doesn't support extension methods. Currently `create_agent` method is defined on `BaseChatClient`, but this method only creates a local instance of `ChatAgent` and it can't create remote agents for providers that support it for a couple of reasons: | ||
|
|
||
| - It's defined as non-async. | ||
| - `BaseChatClient` implementation is stateful for providers like Azure AI or OpenAI Assistants. The implementation stores agent/assistant metadata like `AgentId` and `AgentName`, so currently it's not possible to create different instances of `ChatAgent` from a single `BaseChatClient` in case if the implementation is stateful. | ||
|
|
||
| ## Decision Drivers | ||
|
|
||
| - API should be aligned between .NET and Python. | ||
| - API should be intuitive and consistent between backend providers in .NET and Python. | ||
|
|
||
| ## Considered Options | ||
|
|
||
| Add missing implementations on the Python side. This should include the following: | ||
|
|
||
| ### agent-framework-azure-ai (both V1 and V2) | ||
|
|
||
| - Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`. | ||
dmytrostruk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - Add a `get_agent` method that accepts an agent identifier, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`. | ||
| - Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`. | ||
|
|
||
| .NET: | ||
|
|
||
| ```csharp | ||
| var agent1 = new AIProjectClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from Azure.AI.Projects.OpenAI.AgentReference | ||
| var agent2 = new AIProjectClient(...).GetAIAgent(agentName); // Fetches agent data, creates a local ChatClientAgent instance | ||
| var agent3 = new AIProjectClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance | ||
| ``` | ||
|
|
||
| ### agent-framework-core (OpenAI Assistants) | ||
|
|
||
| - Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`. | ||
| - Add a `get_agent` method that accepts an agent name, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`. | ||
| - Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`. | ||
|
|
||
| .NET: | ||
|
|
||
| ```csharp | ||
| var agent1 = new AssistantClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from OpenAI.Assistants.Assistant | ||
| var agent2 = new AssistantClient(...).GetAIAgent(agentId); // Fetches agent data, creates a local ChatClientAgent instance | ||
| var agent3 = new AssistantClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance | ||
| ``` | ||
|
|
||
| ### Possible Python implementations | ||
|
|
||
| Methods like `create_agent` and `get_agent` should be implemented separately or defined on some stateless component that will allow to create multiple agents from the same instance/place. | ||
|
|
||
| Possible options: | ||
|
|
||
| #### Option 1: Module-level functions | ||
|
|
||
| Implement free functions in the provider package that accept the underlying SDK client as the first argument (similar to .NET extension methods, but expressed in Python). | ||
|
|
||
| Example: | ||
|
|
||
| ```python | ||
| from agent_framework.azure import agents as azure_agents | ||
|
|
||
| ai_project_client = AIProjectClient(...) | ||
|
|
||
| # Creates a remote agent first, then returns a local ChatAgent wrapper | ||
| agent = await azure_agents.create_agent( | ||
| ai_project_client, | ||
| name="", | ||
| instructions="", | ||
| tools=[tool], | ||
| ) | ||
|
|
||
| # Gets an existing remote agent and returns a local ChatAgent wrapper | ||
| agent = await azure_agents.get_agent(ai_project_client, agent_id=agent_id) | ||
|
|
||
| # Wraps an SDK agent instance (no extra HTTP call) | ||
| agent1 = azure_agents.get_agent(ai_project_client, agent_reference) | ||
| ``` | ||
|
|
||
| Pros: | ||
|
|
||
| - Naturally supports async `create_agent` / `get_agent`. | ||
| - Supports multiple agents per SDK client. | ||
| - Closest conceptual match to .NET extension methods while staying Pythonic. | ||
|
|
||
| Cons: | ||
|
|
||
| - Discoverability is lower (users need to know where the functions live). | ||
|
|
||
| #### Option 2: Factory object | ||
|
|
||
| Introduce a dedicated factory type that is constructed from the underlying SDK client, and exposes async `create_agent` / `get_agent` methods. | ||
|
|
||
| Example: | ||
|
|
||
| ```python | ||
| from agent_framework.azure import AgentFactory | ||
|
|
||
| ai_project_client = AIProjectClient(...) | ||
| factory = AgentFactory(ai_project_client) | ||
|
|
||
| agent = await factory.create_agent( | ||
| name="", | ||
| instructions="", | ||
| tools=[tool], | ||
| ) | ||
|
|
||
| agent = await factory.get_agent(agent_id=agent_id) | ||
| agent = factory.get_agent(agent_reference=agent_reference) | ||
| ``` | ||
|
|
||
| Pros: | ||
|
|
||
| - High discoverability and clear grouping of related behavior. | ||
| - Keeps SDK clients unchanged and supports multiple agents per SDK client. | ||
|
|
||
| Cons: | ||
|
|
||
| - Adds a new public concept/type for users to learn. | ||
|
|
||
| #### Option 3: Inheritance (SDK client subclass) | ||
|
|
||
| Create a subclass of the underlying SDK client and add `create_agent` / `get_agent` methods. | ||
|
|
||
| Example: | ||
|
|
||
| ```python | ||
| class ExtendedAIProjectClient(AIProjectClient): | ||
| async def create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent: | ||
| ... | ||
|
|
||
| async def get_agent(self, *, agent_id: str | None = None, sdk_agent=None, **kwargs) -> ChatAgent: | ||
| ... | ||
|
|
||
| client = ExtendedAIProjectClient(...) | ||
| agent = await client.create_agent(name="", instructions="") | ||
| ``` | ||
|
|
||
| Pros: | ||
|
|
||
| - Discoverable and ergonomic call sites. | ||
| - Mirrors the .NET “methods on the client” feeling. | ||
|
|
||
| Cons: | ||
|
|
||
| - Many SDK clients are not designed for inheritance; SDK upgrades can break subclasses. | ||
| - Users must opt into subclass everywhere. | ||
| - Typing/initialization can be tricky if the SDK client has non-trivial constructors. | ||
|
|
||
| #### Option 4: Monkey patching | ||
|
|
||
| Attach `create_agent` / `get_agent` methods to an SDK client class (or instance) at runtime. | ||
|
|
||
| Example: | ||
|
|
||
| ```python | ||
| def _create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent: | ||
| ... | ||
|
|
||
| AIProjectClient.create_agent = _create_agent # monkey patch | ||
| ``` | ||
|
|
||
| Pros: | ||
|
|
||
| - Produces “extension method-like” call sites without wrappers or subclasses. | ||
|
|
||
| Cons: | ||
|
|
||
| - Fragile across SDK updates and difficult to type-check. | ||
| - Surprising behavior (global side effects), potential conflicts across packages. | ||
| - Harder to support/debug, especially in larger apps and test suites. | ||
|
|
||
| ## Decision Outcome | ||
|
|
||
| TBD | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.