diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index cc9ddcbdf4..a03b9489e9 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -85,7 +85,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs
index 60a9b6a973..e2d3ae9459 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Program.cs
@@ -40,7 +40,7 @@ await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedTh
JsonElement reloadedSerializedThread = JsonSerializer.Deserialize(await File.ReadAllTextAsync(tempFilePath));
// Deserialize the thread state after loading from storage.
-AgentThread resumedThread = await agent.DeserializeThreadAsync(reloadedSerializedThread);
+AgentThread resumedThread = agent.DeserializeThread(reloadedSerializedThread);
// Run the agent again with the resumed thread.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
index 8db527a2af..62f32cba67 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
@@ -38,12 +38,12 @@ AIAgent agent = new AzureOpenAIClient(
{
Name = JokerName,
Instructions = JokerInstructions,
- ChatMessageStoreFactory = () =>
+ ChatMessageStoreFactory = (jsonElement, jso) =>
{
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
// also contains the id that the thread is stored under.
- return new VectorChatMessageStore(vectorStore);
+ return new VectorChatMessageStore(vectorStore, jsonElement, jso);
}
});
@@ -65,7 +65,7 @@ Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerO
// and loaded again later.
// Deserialize the thread state after loading from storage.
-AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread);
+AgentThread resumedThread = agent.DeserializeThread(serializedThread);
// Run the agent with the thread that stores conversation history in the vector store a second time.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
@@ -75,10 +75,21 @@ namespace SampleApp
///
/// A sample implementation of that stores chat messages in a vector store.
///
- /// The vector store to store the messages in.
- internal sealed class VectorChatMessageStore(VectorStore vectorStore) : IChatMessageStore
+ internal sealed class VectorChatMessageStore : IChatMessageStore
{
private string? _threadId;
+ private readonly VectorStore _vectorStore;
+
+ public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
+
+ if (serializedStoreState.ValueKind == JsonValueKind.String)
+ {
+ // Here we can deserialize the thread id so that we can access the same messages as before the suspension.
+ this._threadId = JsonSerializer.Deserialize(serializedStoreState);
+ }
+ }
public string? ThreadId => this._threadId;
@@ -86,7 +97,7 @@ namespace SampleApp
{
this._threadId ??= Guid.NewGuid().ToString();
- var collection = vectorStore.GetCollection("ChatHistory");
+ var collection = this._vectorStore.GetCollection("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem()
@@ -101,7 +112,7 @@ namespace SampleApp
public async Task> GetMessagesAsync(CancellationToken cancellationToken)
{
- var collection = vectorStore.GetCollection("ChatHistory");
+ var collection = this._vectorStore.GetCollection("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
var records = await collection
@@ -124,13 +135,6 @@ namespace SampleApp
return new ValueTask(JsonSerializer.SerializeToElement(this._threadId));
}
- public ValueTask DeserializeStateAsync(JsonElement? serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
- {
- // Here we can deserialize the thread id so that we can access the same messages as before the suspension.
- this._threadId = JsonSerializer.Deserialize((JsonElement)serializedStoreState!);
- return new ValueTask();
- }
-
///
/// The data structure used to store chat history items in the vector store.
///
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step13_Memory/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step13_Memory/Program.cs
index c91c81bd4b..df6e9ecb7a 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step13_Memory/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step13_Memory/Program.cs
@@ -38,7 +38,7 @@ ChatClient chatClient = new AzureOpenAIClient(
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
Instructions = "You are a friendly assistant. Always address the user by their name.",
- AIContextProviderFactory = () => new SampleApp.UserInfoMemory(chatClient.AsIChatClient())
+ AIContextProviderFactory = (jse, jso) => new UserInfoMemory(chatClient.AsIChatClient(), jse, jso)
});
// Create a new thread for the conversation.
@@ -57,7 +57,7 @@ var threadElement = await thread.SerializeAsync();
Console.WriteLine("\n>> Use deserialized thread with previously created memories\n");
// Later we can deserialize the thread and continue the conversation with the previous memory component state.
-var deserializedThread = await agent.DeserializeThreadAsync(threadElement);
+var deserializedThread = agent.DeserializeThread(threadElement);
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedThread));
Console.WriteLine("\n>> Read memories from memory component\n");
@@ -89,16 +89,30 @@ namespace SampleApp
///
/// Sample memory component that can remember a user's name and age.
///
- internal sealed class UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null) : AIContextProvider
+ internal sealed class UserInfoMemory : AIContextProvider
{
- public UserInfo UserInfo { get; set; } = userInfo ?? new();
+ private readonly IChatClient _chatClient;
+
+ public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
+ {
+ this._chatClient = chatClient;
+ this.UserInfo = userInfo ?? new UserInfo();
+ }
+
+ public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ this._chatClient = chatClient;
+ this.UserInfo = JsonSerializer.Deserialize(serializedState, jsonSerializerOptions) ?? new UserInfo();
+ }
+
+ public UserInfo UserInfo { get; set; }
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
if ((this.UserInfo.UserName == null || this.UserInfo.UserAge == null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
{
- var result = await chatClient.GetResponseAsync(
+ var result = await this._chatClient.GetResponseAsync(
context.RequestMessages,
new ChatOptions()
{
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs
index 3302593ab0..ea93b001a3 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Specialized/AIAgentHostExecutor.cs
@@ -77,8 +77,7 @@ internal class AIAgentHostExecutor : Executor
JsonElement? threadValue = await context.ReadStateAsync(ThreadStateKey).ConfigureAwait(false);
if (threadValue.HasValue)
{
- this._thread = await this._agent.DeserializeThreadAsync(threadValue.Value, cancellationToken: cancellation)
- .ConfigureAwait(false);
+ this._thread = this._agent.DeserializeThread(threadValue.Value, cancellationToken: cancellation);
}
JsonElement? messagesValue = await context.ReadStateAsync(PendingMessagesStateKey).ConfigureAwait(false);
diff --git a/dotnet/src/Microsoft.Agents.Workflows/WorkflowHostAgent.cs b/dotnet/src/Microsoft.Agents.Workflows/WorkflowHostAgent.cs
index d0a48288d5..53d059522d 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/WorkflowHostAgent.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/WorkflowHostAgent.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -47,6 +48,9 @@ internal class WorkflowHostAgent : AIAgent
public override AgentThread GetNewThread() => new WorkflowThread(this.Id, this.Name, this.GenerateNewId());
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
+ => new WorkflowThread(serializedThread, jsonSerializerOptions);
+
private async
IAsyncEnumerable InvokeStageAsync(
WorkflowThread conversation,
diff --git a/dotnet/src/Microsoft.Agents.Workflows/WorkflowMessageStore.cs b/dotnet/src/Microsoft.Agents.Workflows/WorkflowMessageStore.cs
index dd2aed2750..07e2ec965c 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/WorkflowMessageStore.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/WorkflowMessageStore.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
@@ -14,6 +15,30 @@ internal class WorkflowMessageStore : IChatMessageStore
private int _bookmark = 0;
private readonly List _chatMessages = new();
+ public WorkflowMessageStore()
+ {
+ }
+
+ public WorkflowMessageStore(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ if (serializedStoreState.ValueKind != JsonValueKind.Object)
+ {
+ throw new ArgumentException("The provided JsonElement must be a json object", nameof(serializedStoreState));
+ }
+
+ StoreState? state =
+ JsonSerializer.Deserialize(
+ serializedStoreState,
+ AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState))) as StoreState;
+
+ if (state?.Messages is not null)
+ {
+ this._chatMessages.AddRange(state.Messages);
+ }
+
+ this._bookmark = state?.Bookmark ?? 0;
+ }
+
internal class StoreState
{
public int Bookmark { get; set; }
@@ -50,31 +75,6 @@ internal class WorkflowMessageStore : IChatMessageStore
this._bookmark = this._chatMessages.Count;
}
- public ValueTask DeserializeStateAsync(JsonElement? serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
- {
- if (serializedStoreState is null)
- {
- return default;
- }
-
- object? maybeState =
- JsonSerializer.Deserialize(
- serializedStoreState.Value,
- AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState)));
-
- if (maybeState is not StoreState state)
- {
- throw new JsonException("Invalid state format for WorkflowMessageStore.");
- }
-
- this._chatMessages.Clear();
- this._chatMessages.AddRange(state.Messages);
-
- this._bookmark = state.Bookmark;
-
- return default;
- }
-
public ValueTask SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
StoreState state = new()
diff --git a/dotnet/src/Microsoft.Agents.Workflows/WorkflowThread.cs b/dotnet/src/Microsoft.Agents.Workflows/WorkflowThread.cs
index d8261c77e3..f61e0e63f5 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/WorkflowThread.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/WorkflowThread.cs
@@ -25,6 +25,11 @@ internal class WorkflowThread : AgentThread
this._workflowName = workflowName;
}
+ public WorkflowThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ throw new NotImplementedException("Pending Checkpointing work.");
+ }
+
public string RunId { get; }
public int Halts { get; } = 0;
@@ -35,11 +40,6 @@ internal class WorkflowThread : AgentThread
throw new NotImplementedException("Pending Checkpointing work.");
}
- protected override Task DeserializeAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException("Pending Checkpointing work.");
- }
-
public AgentRunResponseUpdate CreateUpdate(params AIContent[] parts)
{
Throw.IfNullOrEmpty(parts);
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs
index b744a17892..9905d63412 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs
@@ -90,12 +90,8 @@ public abstract class AIAgent
/// Optional to use for deserializing the thread state.
/// The to monitor for cancellation requests. The default is .
/// The deserialized instance.
- public async ValueTask DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
- {
- var thread = this.GetNewThread();
- await thread.DeserializeAsync(serializedThread, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
- return thread;
- }
+ public virtual AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
+ => new(serializedThread, jsonSerializerOptions);
///
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs
index 9b1fbcdb5b..748c5b8f35 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs
@@ -26,6 +26,46 @@ public class AgentThread
{
}
+ ///
+ /// Initializes a new instance of the class from serialized state.
+ ///
+ /// A representing the serialized state of the thread.
+ /// Optional settings for customizing the JSON deserialization process.
+ /// An optional factory function to create a custom .
+ /// An optional factory function to create a custom .
+ public AgentThread(
+ JsonElement serializedThreadState,
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ Func? chatMessageStoreFactory = null,
+ Func? aiContextProviderFactory = null)
+ {
+ if (serializedThreadState.ValueKind != JsonValueKind.Object)
+ {
+ throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState));
+ }
+
+ var state = JsonSerializer.Deserialize(
+ serializedThreadState,
+ AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))) as ThreadState;
+
+ this.AIContextProvider = aiContextProviderFactory?.Invoke(state?.AIContextProviderState ?? default, jsonSerializerOptions);
+
+ if (state?.ConversationId is string threadId)
+ {
+ this.ConversationId = threadId;
+
+ // Since we have an ID, we should not have a chat message store and we can return here.
+ return;
+ }
+
+ this._messageStore = chatMessageStoreFactory?.Invoke(state?.StoreState ?? default, jsonSerializerOptions);
+ if (this._messageStore is null)
+ {
+ // If we didn't get a custom store, create an in-memory one.
+ this._messageStore = new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions);
+ }
+ }
+
///
/// Gets or sets the ID of the underlying service thread to support cases where the chat history is stored by the agent service.
///
@@ -175,47 +215,6 @@ public class AgentThread
}
}
- ///
- /// Deserializes the state contained in the provided into the properties on this thread.
- ///
- /// A representing the state of the thread.
- /// Optional settings for customizing the JSON deserialization process.
- /// The to monitor for cancellation requests. The default is .
- /// A that completes when the state has been deserialized.
- protected internal virtual async Task DeserializeAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
- {
- var state = JsonSerializer.Deserialize(
- serializedThread,
- AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))) as ThreadState;
-
- if (state?.ConversationId is string threadId)
- {
- this.ConversationId = threadId;
-
- // Since we have an ID, we should not have a chat message store and we can return here.
- return;
- }
-
- if (state?.AIContextProviderState.HasValue is true && this.AIContextProvider is not null)
- {
- await this.AIContextProvider.DeserializeAsync(state.AIContextProviderState.Value, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
- }
-
- // If we don't have any IChatMessageStore state return here.
- if (state?.StoreState is null || state?.StoreState.Value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
- {
- return;
- }
-
- if (this._messageStore is null)
- {
- // If we don't have a chat message store yet, create an in-memory one.
- this._messageStore = new InMemoryChatMessageStore();
- }
-
- await this._messageStore.DeserializeStateAsync(state!.StoreState.Value, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
- }
-
internal sealed class ThreadState
{
public string? ConversationId { get; set; }
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/DelegatingAIAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/DelegatingAIAgent.cs
index 81cc93628c..d79cf03914 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/DelegatingAIAgent.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/DelegatingAIAgent.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
@@ -52,6 +53,10 @@ public class DelegatingAIAgent : AIAgent
///
public override AgentThread GetNewThread() => this.InnerAgent.GetNewThread();
+ ///
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
+ => this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions, cancellationToken);
+
///
public override Task RunAsync(
IEnumerable messages,
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs
index e9c3bf374a..5990002fe1 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs
@@ -44,28 +44,11 @@ public interface IChatMessageStore
/// An async task.
Task AddMessagesAsync(IEnumerable messages, CancellationToken cancellationToken = default);
- ///
- /// Deserializes the state contained in the provided into the properties on this store.
- ///
- /// A representing the state of the store.
- /// Optional settings for customizing the JSON deserialization process.
- /// The to monitor for cancellation requests. The default is .
- /// A that completes when the state has been deserialized.
- ///
- /// This method, together with can be used to save and load messages from a persistent store
- /// if this store only has messages in memory.
- ///
- ValueTask DeserializeStateAsync(JsonElement? serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
-
///
/// Serializes the current object's state to a using the specified serialization options.
///
/// The JSON serialization options to use.
/// The to monitor for cancellation requests. The default is .
/// A representation of the object's state.
- ///
- /// This method, together with can be used to save and load messages from a persistent store
- /// if this store only has messages in memory.
- ///
ValueTask SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs
index 6f59fd696c..f57d0a8acc 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs
@@ -17,12 +17,23 @@ public sealed class InMemoryChatMessageStore : IList, IChatMessageS
{
private readonly IChatReducer? _chatReducer;
private readonly ChatReducerTriggerEvent _reducerTriggerEvent;
- private List _messages = new();
+ private List _messages;
///
/// Initializes a new instance of the class.
///
public InMemoryChatMessageStore()
+ {
+ this._messages = new();
+ }
+
+ ///
+ /// Initializes a new instance of the class, with an existing state from a serialized JSON element.
+ ///
+ /// A representing the serialized state of the store.
+ /// Optional settings for customizing the JSON deserialization process.
+ public InMemoryChatMessageStore(JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
+ : this(null, serializedStoreState, jsonSerializerOptions, ChatReducerTriggerEvent.BeforeMessagesRetrieval)
{
}
@@ -32,9 +43,40 @@ public sealed class InMemoryChatMessageStore : IList, IChatMessageS
/// An optional instance used to process or reduce chat messages. If null, no reduction logic will be applied.
/// The event that should trigger the reducer invocation.
public InMemoryChatMessageStore(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
+ : this(chatReducer, default, null, reducerTriggerEvent)
{
- this._chatReducer = Throw.IfNull(chatReducer);
+ Throw.IfNull(chatReducer);
+ }
+
+ ///
+ /// Initializes a new instance of the class, with an existing state from a serialized JSON element.
+ ///
+ /// An optional instance used to process or reduce chat messages. If null, no reduction logic will be applied.
+ /// A representing the serialized state of the store.
+ /// Optional settings for customizing the JSON deserialization process.
+ /// The event that should trigger the reducer invocation.
+ public InMemoryChatMessageStore(IChatReducer? chatReducer, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
+ {
+ this._chatReducer = chatReducer;
this._reducerTriggerEvent = reducerTriggerEvent;
+
+ if (serializedStoreState.ValueKind != JsonValueKind.Object)
+ {
+ this._messages = new();
+ return;
+ }
+
+ var state = JsonSerializer.Deserialize(
+ serializedStoreState,
+ AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState))) as StoreState;
+
+ if (state?.Messages is { Count: > 0 } messages)
+ {
+ this._messages = messages;
+ return;
+ }
+
+ this._messages = new();
}
///
@@ -84,26 +126,6 @@ public sealed class InMemoryChatMessageStore : IList, IChatMessageS
return this._messages;
}
- ///
- public ValueTask DeserializeStateAsync(JsonElement? serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
- {
- if (serializedStoreState is null)
- {
- return default;
- }
-
- var state = JsonSerializer.Deserialize(
- serializedStoreState.Value,
- AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState))) as StoreState;
-
- if (state?.Messages is { Count: > 0 } messages)
- {
- this._messages.AddRange(messages);
- }
-
- return default;
- }
-
///
public ValueTask SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
@@ -157,7 +179,7 @@ public sealed class InMemoryChatMessageStore : IList, IChatMessageS
internal sealed class StoreState
{
- public IList Messages { get; set; } = new List();
+ public List Messages { get; set; } = new List();
}
///
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentActor.cs
index 31d91c1c6d..328d8a5ad8 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentActor.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentActor.cs
@@ -39,7 +39,7 @@ internal sealed class AgentActor(
if (threadResult.Value is { } threadJson)
{
// Deserialize the thread state if it exists
- this._thread = await agent.DeserializeThreadAsync(threadJson, cancellationToken: cancellationToken).ConfigureAwait(false);
+ this._thread = agent.DeserializeThread(threadJson, cancellationToken: cancellationToken);
hasExistingThread = true;
}
}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxy.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxy.cs
index 5a0b41bb8a..97588d3779 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxy.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxy.cs
@@ -36,6 +36,10 @@ public sealed class AgentProxy : AIAgent
///
public override AgentThread GetNewThread() => new AgentProxyThread();
+ ///
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
+ => new AgentProxyThread(serializedThread, jsonSerializerOptions);
+
///
/// Gets a thread by its .
///
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxyThread.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxyThread.cs
index 83cea8eabb..d401016aba 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxyThread.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Hosting/AgentProxyThread.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.Shared.Diagnostics;
@@ -55,6 +56,16 @@ internal sealed partial class AgentProxyThread : AgentThread
{
}
+ ///
+ /// Initializes a new instance of the class from serialized state.
+ ///
+ /// A representing the serialized state of the thread.
+ /// Optional settings for customizing the JSON deserialization process.
+ public AgentProxyThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
+ : base(serializedThreadState, jsonSerializerOptions)
+ {
+ }
+
internal static string CreateId() => Guid.NewGuid().ToString("N");
///
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs
index e583d5d4e5..5e22814402 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
@@ -83,6 +84,10 @@ public class OpenAIChatClientAgent : AIAgent
public sealed override AgentThread GetNewThread()
=> this._chatClientAgent.GetNewThread();
+ ///
+ public sealed override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
+ => this._chatClientAgent.DeserializeThread(serializedThread, jsonSerializerOptions, cancellationToken);
+
///
public sealed override Task RunAsync(
IEnumerable messages,
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs
index 5878cc71c9..982cb2799a 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -241,12 +242,16 @@ public sealed class ChatClientAgent : AIAgent
{
var thread = new AgentThread
{
- MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke(),
- AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke()
+ MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke(default, null),
+ AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(default, null)
};
return thread;
}
+ ///
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
+ => new(serializedThread, jsonSerializerOptions, this._agentOptions?.ChatMessageStoreFactory, this._agentOptions?.AIContextProviderFactory);
+
#region Private
///
@@ -493,7 +498,7 @@ public sealed class ChatClientAgent : AIAgent
// If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and
// the thread has no MessageStore yet, and we have a custom messages store, we should update the thread
// with the custom MessageStore so that it has somewhere to store the chat history.
- thread.MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke();
+ thread.MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke(default, null);
}
}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs
index 461ed01aa3..2b3807864d 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Text.Json;
namespace Microsoft.Extensions.AI.Agents;
@@ -78,14 +79,14 @@ public class ChatClientAgentOptions
/// Gets or sets a factory function to create an instance of
/// which will be used to store chat messages for this agent.
///
- public Func? ChatMessageStoreFactory { get; set; }
+ public Func? ChatMessageStoreFactory { get; set; }
///
/// Gets or sets a factory function to create an instance of
/// which will be used to create a context provider for each new thread, and can then
/// provide additional context for each agent run.
///
- public Func? AIContextProviderFactory { get; set; }
+ public Func? AIContextProviderFactory { get; set; }
///
/// Gets or sets a value indicating whether to use the provided instance as is,
diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs
index 843ea3fb0f..b22b7fc51a 100644
--- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs
+++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs
@@ -135,40 +135,39 @@ public class AgentThreadTests
#region Deserialize Tests
[Fact]
- public async Task VerifyDeserializeWithMessagesAsync()
+ public async Task VerifyDeserializeConstructorWithMessagesAsync()
{
// Arrange
- var chatMessageStore = new InMemoryChatMessageStore();
var json = JsonSerializer.Deserialize("""
{
"storeState": { "messages": [{"authorName": "testAuthor"}] }
}
""", TestJsonSerializerContext.Default.JsonElement);
- var thread = new AgentThread { MessageStore = chatMessageStore };
// Act.
- await thread.DeserializeAsync(json);
+ var thread = new AgentThread(json);
// Assert
Assert.Null(thread.ConversationId);
- Assert.Single(chatMessageStore);
- Assert.Equal("testAuthor", chatMessageStore[0].AuthorName);
+ var messageStore = thread.MessageStore as InMemoryChatMessageStore;
+ Assert.NotNull(messageStore);
+ Assert.Single(messageStore);
+ Assert.Equal("testAuthor", messageStore[0].AuthorName);
}
[Fact]
- public async Task VerifyDeserializeWithIdAsync()
+ public async Task VerifyDeserializeConstructorWithIdAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
{
- "conversationId": "TestConvId"
+ "ConversationId": "TestConvId"
}
""", TestJsonSerializerContext.Default.JsonElement);
- var thread = new AgentThread();
// Act
- await thread.DeserializeAsync(json);
+ var thread = new AgentThread(json);
// Assert
Assert.Equal("TestConvId", thread.ConversationId);
@@ -176,34 +175,33 @@ public class AgentThreadTests
}
[Fact]
- public async Task VerifyDeserializeWithAIContextProviderAsync()
+ public async Task VerifyDeserializeConstructorWithAIContextProviderAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
{
+ "ConversationId": "TestConvId",
"aiContextProviderState": ["CP1"]
}
""", TestJsonSerializerContext.Default.JsonElement);
Mock mockProvider = new();
- var thread = new AgentThread() { AIContextProvider = mockProvider.Object };
// Act
- await thread.DeserializeAsync(json);
+ var thread = new AgentThread(json, aiContextProviderFactory: (_, _) => mockProvider.Object);
// Assert
Assert.Null(thread.MessageStore);
- mockProvider.Verify(m => m.DeserializeAsync(It.Is(e => e.ValueKind == JsonValueKind.Array && e.GetArrayLength() == 1), It.IsAny(), It.IsAny()), Times.Once);
+ Assert.Same(thread.AIContextProvider, mockProvider.Object);
}
[Fact]
- public async Task DeserializeWithInvalidJsonThrowsAsync()
+ public async Task DeserializeContructorWithInvalidJsonThrowsAsync()
{
// Arrange
var invalidJson = JsonSerializer.Deserialize("[42]", TestJsonSerializerContext.Default.JsonElement);
- var thread = new AgentThread();
// Act & Assert
- await Assert.ThrowsAsync(() => thread.DeserializeAsync(invalidJson));
+ Assert.Throws(() => new AgentThread(invalidJson));
}
#endregion Deserialize Tests
diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs
index 8b1503157f..98bd4caa46 100644
--- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs
+++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs
@@ -89,19 +89,17 @@ public class InMemoryChatMessageStoreTests
}
[Fact]
- public async Task DeserializeWithEmptyElementAsync()
+ public async Task DeserializeConstructorWithEmptyElementAsync()
{
- var newStore = new InMemoryChatMessageStore();
-
var emptyObject = JsonSerializer.Deserialize("{}", TestJsonSerializerContext.Default.JsonElement);
- await newStore.DeserializeStateAsync(emptyObject);
+ var newStore = new InMemoryChatMessageStore(emptyObject);
Assert.Empty(newStore);
}
[Fact]
- public async Task SerializeAndDeserializeRoundtripsAsync()
+ public async Task SerializeAndDeserializeConstructorRoundtripsAsync()
{
var store = new InMemoryChatMessageStore
{
@@ -110,9 +108,7 @@ public class InMemoryChatMessageStoreTests
};
var jsonElement = await store.SerializeStateAsync();
- var newStore = new InMemoryChatMessageStore();
-
- await newStore.DeserializeStateAsync(jsonElement);
+ var newStore = new InMemoryChatMessageStore(jsonElement.Value);
Assert.Equal(2, newStore.Count);
Assert.Equal("A", newStore[0].Text);
@@ -141,57 +137,49 @@ public class InMemoryChatMessageStoreTests
}
[Fact]
- public async Task DeserializeStateAsync_WithNullSerializedState_DoesNothingAsync()
+ public void DeserializeContructor_WithNullSerializedState_CreatesEmptyStore()
{
- // Arrange
- var store = new InMemoryChatMessageStore();
- store.Add(new ChatMessage(ChatRole.User, "Existing message"));
-
// Act
- await store.DeserializeStateAsync(null);
+ var store = new InMemoryChatMessageStore(new JsonElement());
- // Assert - Should still have the existing message
- Assert.Single(store);
- Assert.Equal("Existing message", store[0].Text);
+ // Assert
+ Assert.Empty(store);
}
[Fact]
- public async Task DeserializeStateAsync_WithEmptyMessages_DoesNotAddMessagesAsync()
+ public async Task DeserializeContructor_WithEmptyMessages_DoesNotAddMessagesAsync()
{
// Arrange
- var store = new InMemoryChatMessageStore();
var stateWithEmptyMessages = JsonSerializer.SerializeToElement(
new Dictionary { ["Messages"] = new List() },
TestJsonSerializerContext.Default.IDictionaryStringObject);
// Act
- await store.DeserializeStateAsync(stateWithEmptyMessages);
+ var store = new InMemoryChatMessageStore(stateWithEmptyMessages);
// Assert
Assert.Empty(store);
}
[Fact]
- public async Task DeserializeStateAsync_WithNullMessages_DoesNotAddMessagesAsync()
+ public async Task DeserializeConstructor_WithNullMessages_DoesNotAddMessagesAsync()
{
// Arrange
- var store = new InMemoryChatMessageStore();
var stateWithNullMessages = JsonSerializer.SerializeToElement(
new Dictionary { ["Messages"] = null! },
TestJsonSerializerContext.Default.DictionaryStringObject);
// Act
- await store.DeserializeStateAsync(stateWithNullMessages);
+ var store = new InMemoryChatMessageStore(stateWithNullMessages);
// Assert
Assert.Empty(store);
}
[Fact]
- public async Task DeserializeStateAsync_WithValidMessages_AddsMessagesAsync()
+ public async Task DeserializeConstructor_WithValidMessages_AddsMessagesAsync()
{
// Arrange
- var store = new InMemoryChatMessageStore();
var messages = new List
{
new(ChatRole.User, "User message"),
@@ -203,7 +191,7 @@ public class InMemoryChatMessageStoreTests
TestJsonSerializerContext.Default.DictionaryStringObject);
// Act
- await store.DeserializeStateAsync(serializedState);
+ var store = new InMemoryChatMessageStore(serializedState);
// Assert
Assert.Equal(2, store.Count);
diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentOptionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentOptionsTests.cs
index d8dd5d52fc..2e9192a56a 100644
--- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentOptionsTests.cs
+++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentOptionsTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Text.Json;
using Moq;
namespace Microsoft.Extensions.AI.Agents.UnitTests.ChatCompletion;
@@ -164,8 +165,8 @@ public class ChatClientAgentOptionsTests
const string Name = "Test name";
const string Description = "Test description";
var tools = new List { AIFunctionFactory.Create(() => "test") };
- static IChatMessageStore ChatMessageStoreFactory() => new Mock().Object;
- static AIContextProvider AIContextProviderFactory() => new Mock().Object;
+ static IChatMessageStore ChatMessageStoreFactory(JsonElement jse, JsonSerializerOptions? jso) => new Mock().Object;
+ static AIContextProvider AIContextProviderFactory(JsonElement jse, JsonSerializerOptions? jso) => new Mock().Object;
var original = new ChatClientAgentOptions(Instructions, Name, Description, tools)
{
diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs
index 4ec63e04cd..420567acdc 100644
--- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs
+++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs
@@ -461,7 +461,7 @@ public class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny(), It.IsAny()))
.Returns(new ValueTask());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = () => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = (_, _) => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await agent.RunAsync(requestMessages);
@@ -506,7 +506,7 @@ public class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny(), It.IsAny()))
.Returns(new ValueTask());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = () => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = (_, _) => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await Assert.ThrowsAsync(() => agent.RunAsync(requestMessages));
@@ -548,7 +548,7 @@ public class ChatClientAgentTests
.Setup(p => p.InvokingAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new AIContext());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = () => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = (_, _) => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await agent.RunAsync([new(ChatRole.User, "user message")]);
@@ -1713,7 +1713,7 @@ public class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions",
- ChatMessageStoreFactory = () =>
+ ChatMessageStoreFactory = (_, _) =>
{
factoryCalled = true;
return mockStore.Object;
@@ -1738,7 +1738,7 @@ public class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions",
- AIContextProviderFactory = () =>
+ AIContextProviderFactory = (_, _) =>
{
factoryCalled = true;
return mockContextProvider.Object;
diff --git a/python/packages/main/agent_framework/openai/_chat_client.py b/python/packages/main/agent_framework/openai/_chat_client.py
index aa98257804..450323504a 100644
--- a/python/packages/main/agent_framework/openai/_chat_client.py
+++ b/python/packages/main/agent_framework/openai/_chat_client.py
@@ -427,7 +427,9 @@ class OpenAIChatClient(OpenAIConfigMixin, OpenAIBaseChatClient):
async_client: An existing client to use. (Optional)
instruction_role: The role to use for 'instruction' messages, for example,
"system" or "developer". If not provided, the default is "system".
- base_url: The optional base URL to use. If provided will override the standard value for a OpenAI connector.
+ base_url: The optional base URL to use. If provided will override
+ the standard value for a OpenAI connector,
+ the env vars or .env file value.
env_file_path: Use the environment settings file as a fallback
to environment variables. (Optional)
env_file_encoding: The encoding of the environment settings file. (Optional)
@@ -435,6 +437,7 @@ class OpenAIChatClient(OpenAIConfigMixin, OpenAIBaseChatClient):
try:
openai_settings = OpenAISettings(
api_key=SecretStr(api_key) if api_key else None,
+ base_url=base_url,
org_id=org_id,
chat_model_id=ai_model_id,
env_file_path=env_file_path,
@@ -456,11 +459,11 @@ class OpenAIChatClient(OpenAIConfigMixin, OpenAIBaseChatClient):
super().__init__(
ai_model_id=openai_settings.chat_model_id,
api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None,
+ base_url=openai_settings.base_url if openai_settings.base_url else None,
org_id=openai_settings.org_id,
default_headers=default_headers,
client=async_client,
instruction_role=instruction_role,
- base_url=base_url,
)
@classmethod
diff --git a/python/packages/main/agent_framework/openai/_shared.py b/python/packages/main/agent_framework/openai/_shared.py
index de8ada2ca4..9d5ab7e52f 100644
--- a/python/packages/main/agent_framework/openai/_shared.py
+++ b/python/packages/main/agent_framework/openai/_shared.py
@@ -78,6 +78,8 @@ class OpenAISettings(AFBaseSettings):
Attributes:
api_key: OpenAI API key, see https://platform.openai.com/account/api-keys
(Env var OPENAI_API_KEY)
+ base_url: The base URL for the OpenAI API.
+ (Env var OPENAI_BASE_URL)
org_id: This is usually optional unless your account belongs to multiple organizations.
(Env var OPENAI_ORG_ID)
chat_model_id: The OpenAI chat model ID to use, for example, gpt-3.5-turbo or gpt-4.
@@ -106,6 +108,7 @@ class OpenAISettings(AFBaseSettings):
env_prefix: ClassVar[str] = "OPENAI_"
api_key: SecretStr | None = None
+ base_url: str | None = None
org_id: str | None = None
chat_model_id: str | None = None
responses_model_id: str | None = None
diff --git a/python/packages/main/tests/openai/test_openai_chat_client.py b/python/packages/main/tests/openai/test_openai_chat_client.py
index f57af48207..62c7bbf948 100644
--- a/python/packages/main/tests/openai/test_openai_chat_client.py
+++ b/python/packages/main/tests/openai/test_openai_chat_client.py
@@ -80,6 +80,22 @@ def test_init_base_url(openai_unit_test_env: dict[str, str]) -> None:
assert str(open_ai_chat_completion.client.base_url) == "http://localhost:1234/v1/"
+def test_init_base_url_from_settings_env() -> None:
+ """Test that base_url from OpenAISettings environment variable is properly used."""
+ # Set environment variable for base_url
+ with patch.dict(
+ os.environ,
+ {
+ "OPENAI_API_KEY": "dummy",
+ "OPENAI_CHAT_MODEL_ID": "gpt-5",
+ "OPENAI_BASE_URL": "https://custom-openai-endpoint.com/v1",
+ },
+ ):
+ client = OpenAIChatClient()
+ assert client.ai_model_id == "gpt-5"
+ assert str(client.client.base_url) == "https://custom-openai-endpoint.com/v1/"
+
+
@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True)
def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
with pytest.raises(ServiceInitializationError):