Skip to content

Commit 9d86adf

Browse files
westey-mCopilot
andauthored
.NET: Update AgentFeatureCollections with feedback (#2379)
* Update AgentFeatureCollections with feedback * Address feedback. * Fix issue with sample. * Change generic type restriction to notnull * Remove revision * Update dotnet/src/Microsoft.Agents.AI.Abstractions/Features/AgentFeatureCollectionExtensions.cs Co-authored-by: Copilot <[email protected]> * Add revision back again and improve some formatting. * Remove virtual from revision. * Add overloads taking type as param and add unit tests. --------- Co-authored-by: Copilot <[email protected]>
1 parent 570bed9 commit 9d86adf

File tree

9 files changed

+291
-141
lines changed

9 files changed

+291
-141
lines changed

dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,8 @@ async Task CustomChatMessageStore_UsingFactoryAndExistingExternalId_Async()
118118

119119
// It's possible to create a new thread that uses the same chat message store id by providing
120120
// the VectorChatMessageStoreThreadDbKeyFeature in the feature collection when creating the new thread.
121-
AgentFeatureCollection features = new();
122-
features.Set(new VectorChatMessageStoreThreadDbKeyFeature(messageStoreFromFactory.ThreadDbKey!));
123-
AgentThread resumedThread = agent.GetNewThread(features);
121+
AgentThread resumedThread = agent.GetNewThread(
122+
new AgentFeatureCollection().WithFeature(new VectorChatMessageStoreThreadDbKeyFeature(messageStoreFromFactory.ThreadDbKey!)));
124123

125124
// Run the agent with the thread that stores conversation history in the vector store.
126125
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
@@ -149,9 +148,7 @@ async Task CustomChatMessageStore_PerThread_Async()
149148
// We can then pass the feature collection when creating a new thread.
150149
// We also have the opportunity here to pass any id that we want for storing the chat history in the vector store.
151150
VectorChatMessageStore perThreadMessageStore = new(vectorStore, "chat-history-1");
152-
AgentFeatureCollection features = new();
153-
features.Set<ChatMessageStore>(perThreadMessageStore);
154-
AgentThread thread = agent.GetNewThread(features);
151+
AgentThread thread = agent.GetNewThread(new AgentFeatureCollection().WithFeature<ChatMessageStore>(perThreadMessageStore));
155152

156153
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
157154

@@ -191,10 +188,13 @@ async Task CustomChatMessageStore_PerRun_Async()
191188
// If the agent doesn't support a message store, it would be ignored.
192189
// We also have the opportunity here to pass any id that we want for storing the chat history in the vector store.
193190
VectorChatMessageStore perRunMessageStore = new(vectorStore, "chat-history-1");
194-
AgentFeatureCollection features = new();
195-
features.Set<ChatMessageStore>(perRunMessageStore);
196-
197-
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread, options: new AgentRunOptions() { Features = features }));
191+
Console.WriteLine(await agent.RunAsync(
192+
"Tell me a joke about a pirate.",
193+
thread,
194+
options: new AgentRunOptions()
195+
{
196+
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(perRunMessageStore)
197+
}));
198198

199199
// When serializing this thread, we'll see that it has no messagestore state, since the messagestore was not attached to the thread,
200200
// but just provided for the single run. Note that, depending on the circumstances, the thread may still contain other state, e.g. Memories,
@@ -237,8 +237,9 @@ public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedSto
237237
// or finally we can generate one ourselves.
238238
this.ThreadDbKey = serializedStoreState.ValueKind is JsonValueKind.String
239239
? serializedStoreState.Deserialize<string>()
240-
: features?.Get<VectorChatMessageStoreThreadDbKeyFeature>()?.ThreadDbKey
241-
?? Guid.NewGuid().ToString("N");
240+
: features?.TryGet<VectorChatMessageStoreThreadDbKeyFeature>(out var threadDbKeyFeature) is true
241+
? threadDbKeyFeature.ThreadDbKey
242+
: Guid.NewGuid().ToString("N");
242243
}
243244

244245
public string? ThreadDbKey { get; }

dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, str
5555

5656
/// <inheritdoc/>
5757
public sealed override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
58-
=> new A2AAgentThread() { ContextId = featureCollection?.Get<ConversationIdAgentFeature>()?.ConversationId };
58+
=> new A2AAgentThread()
59+
{
60+
ContextId = featureCollection?.TryGet<ConversationIdAgentFeature>(out var conversationIdFeature) is true
61+
? conversationIdFeature.ConversationId
62+
: null
63+
};
5964

6065
/// <summary>
6166
/// Get a new <see cref="AgentThread"/> instance using an existing context id, to continue that conversation.

dotnet/src/Microsoft.Agents.AI.Abstractions/Features/AgentFeatureCollection.cs

Lines changed: 86 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Linq;
89
using Microsoft.Shared.Diagnostics;
910

@@ -18,9 +19,7 @@ namespace Microsoft.Agents.AI;
1819
[DebuggerTypeProxy(typeof(FeatureCollectionDebugView))]
1920
public class AgentFeatureCollection : IAgentFeatureCollection
2021
{
21-
private static readonly KeyComparer s_featureKeyComparer = new();
22-
private readonly IAgentFeatureCollection? _defaults;
23-
private readonly int _initialCapacity;
22+
private readonly IAgentFeatureCollection? _innerCollection;
2423
private Dictionary<Type, object>? _features;
2524
private volatile int _containerRevision;
2625

@@ -39,124 +38,142 @@ public AgentFeatureCollection()
3938
public AgentFeatureCollection(int initialCapacity)
4039
{
4140
Throw.IfLessThan(initialCapacity, 0);
42-
43-
this._initialCapacity = initialCapacity;
41+
this._features = new(initialCapacity);
4442
}
4543

4644
/// <summary>
47-
/// Initializes a new instance of <see cref="AgentFeatureCollection"/> with the specified defaults.
45+
/// Initializes a new instance of <see cref="AgentFeatureCollection"/> with the specified inner collection.
4846
/// </summary>
49-
/// <param name="defaults">The feature defaults.</param>
50-
public AgentFeatureCollection(IAgentFeatureCollection defaults)
47+
/// <param name="innerCollection">The inner collection.</param>
48+
/// <remarks>
49+
/// <para>
50+
/// When providing an inner collection, and if a feature is not found in this collection,
51+
/// an attempt will be made to retrieve it from the inner collection as a fallback.
52+
/// </para>
53+
/// <para>
54+
/// The <see cref="Remove{TFeature}"/> method will only remove features from this collection
55+
/// and not from the inner collection. When removing a feature from this collection, and
56+
/// it exists in the inner collection, it will still be retrievable from the inner collection.
57+
/// </para>
58+
/// </remarks>
59+
public AgentFeatureCollection(IAgentFeatureCollection innerCollection)
5160
{
52-
this._defaults = defaults;
61+
this._innerCollection = Throw.IfNull(innerCollection);
5362
}
5463

5564
/// <inheritdoc />
56-
public virtual int Revision
65+
public int Revision
5766
{
58-
get { return this._containerRevision + (this._defaults?.Revision ?? 0); }
67+
get { return this._containerRevision + (this._innerCollection?.Revision ?? 0); }
5968
}
6069

6170
/// <inheritdoc />
6271
public bool IsReadOnly { get { return false; } }
6372

73+
IEnumerator IEnumerable.GetEnumerator()
74+
{
75+
return this.GetEnumerator();
76+
}
77+
6478
/// <inheritdoc />
65-
public object? this[Type key]
79+
public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
6680
{
67-
get
81+
if (this._features is not { Count: > 0 })
6882
{
69-
Throw.IfNull(key);
83+
IEnumerable<KeyValuePair<Type, object>> e = ((IEnumerable<KeyValuePair<Type, object>>?)this._innerCollection) ?? [];
84+
return e.GetEnumerator();
85+
}
7086

71-
return this._features != null && this._features.TryGetValue(key, out var result) ? result : this._defaults?[key];
87+
if (this._innerCollection is null)
88+
{
89+
return this._features.GetEnumerator();
7290
}
73-
set
91+
92+
if (this._innerCollection is AgentFeatureCollection innerCollection && innerCollection._features is not { Count: > 0 })
7493
{
75-
Throw.IfNull(key);
94+
return this._features.GetEnumerator();
95+
}
96+
97+
return YieldAll();
7698

77-
if (value == null)
99+
IEnumerator<KeyValuePair<Type, object>> YieldAll()
100+
{
101+
HashSet<Type> set = [];
102+
103+
foreach (var entry in this._features)
78104
{
79-
if (this._features?.Remove(key) is true)
80-
{
81-
this._containerRevision++;
82-
}
83-
return;
105+
set.Add(entry.Key);
106+
yield return entry;
84107
}
85108

86-
if (this._features == null)
109+
foreach (var entry in this._innerCollection.Where(x => !set.Contains(x.Key)))
87110
{
88-
this._features = new Dictionary<Type, object>(this._initialCapacity);
111+
yield return entry;
89112
}
90-
this._features[key] = value;
91-
this._containerRevision++;
92113
}
93114
}
94115

95-
IEnumerator IEnumerable.GetEnumerator()
96-
{
97-
return this.GetEnumerator();
98-
}
99-
100116
/// <inheritdoc />
101-
public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
117+
public bool TryGet<TFeature>([MaybeNullWhen(false)] out TFeature feature)
118+
where TFeature : notnull
102119
{
103-
if (this._features != null)
120+
if (this.TryGet(typeof(TFeature), out var obj))
104121
{
105-
foreach (var pair in this._features)
106-
{
107-
yield return pair;
108-
}
122+
feature = (TFeature)obj;
123+
return true;
109124
}
110125

111-
if (this._defaults != null)
112-
{
113-
// Don't return features masked by the wrapper.
114-
foreach (var pair in this._features == null ? this._defaults : this._defaults.Except(this._features, s_featureKeyComparer))
115-
{
116-
yield return pair;
117-
}
118-
}
126+
feature = default;
127+
return false;
119128
}
120129

121130
/// <inheritdoc />
122-
public TFeature? Get<TFeature>()
131+
public bool TryGet(Type type, [MaybeNullWhen(false)] out object feature)
123132
{
124-
if (typeof(TFeature).IsValueType)
133+
if (this._features?.TryGetValue(type, out var obj) is true)
125134
{
126-
var feature = this[typeof(TFeature)];
127-
if (feature is null && Nullable.GetUnderlyingType(typeof(TFeature)) is null)
128-
{
129-
throw new InvalidOperationException(
130-
$"{typeof(TFeature).FullName} does not exist in the feature collection " +
131-
$"and because it is a struct the method can't return null. Use 'AgentFeatureCollection[typeof({typeof(TFeature).FullName})] is not null' to check if the feature exists.");
132-
}
133-
return (TFeature?)feature;
135+
feature = obj;
136+
return true;
134137
}
135-
return (TFeature?)this[typeof(TFeature)];
138+
139+
if (this._innerCollection?.TryGet(type, out var defaultFeature) is true)
140+
{
141+
feature = defaultFeature;
142+
return true;
143+
}
144+
145+
feature = default;
146+
return false;
136147
}
137148

138149
/// <inheritdoc />
139-
public void Set<TFeature>(TFeature? instance)
150+
public void Set<TFeature>(TFeature instance)
151+
where TFeature : notnull
140152
{
141-
this[typeof(TFeature)] = instance;
153+
Throw.IfNull(instance);
154+
155+
this._features ??= new();
156+
this._features[typeof(TFeature)] = instance;
157+
this._containerRevision++;
142158
}
143159

144-
// Used by the debugger. Count over enumerable is required to get the correct value.
145-
private int GetCount() => this.Count();
160+
/// <inheritdoc />
161+
public void Remove<TFeature>()
162+
where TFeature : notnull
163+
=> this.Remove(typeof(TFeature));
146164

147-
private sealed class KeyComparer : IEqualityComparer<KeyValuePair<Type, object>>
165+
/// <inheritdoc />
166+
public void Remove(Type type)
148167
{
149-
public bool Equals(KeyValuePair<Type, object> x, KeyValuePair<Type, object> y)
168+
if (this._features?.Remove(type) is true)
150169
{
151-
return x.Key.Equals(y.Key);
152-
}
153-
154-
public int GetHashCode(KeyValuePair<Type, object> obj)
155-
{
156-
return obj.Key.GetHashCode();
170+
this._containerRevision++;
157171
}
158172
}
159173

174+
// Used by the debugger. Count over enumerable is required to get the correct value.
175+
private int GetCount() => this.Count();
176+
160177
private sealed class FeatureCollectionDebugView(AgentFeatureCollection features)
161178
{
162179
private readonly AgentFeatureCollection _features = features;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
namespace Microsoft.Agents.AI;
4+
5+
/// <summary>
6+
/// Extension methods for <see cref="IAgentFeatureCollection"/>.
7+
/// </summary>
8+
public static class AgentFeatureCollectionExtensions
9+
{
10+
/// <summary>
11+
/// Adds the specified feature to the collection and returns the collection.
12+
/// </summary>
13+
/// <typeparam name="TFeature">The feature key.</typeparam>
14+
/// <param name="features">The feature collection to add the new feature to.</param>
15+
/// <param name="feature">The feature to add to the collection.</param>
16+
/// <returns>The updated collection.</returns>
17+
public static IAgentFeatureCollection WithFeature<TFeature>(this IAgentFeatureCollection features, TFeature feature)
18+
where TFeature : notnull
19+
{
20+
features.Set(feature);
21+
return features;
22+
}
23+
}

dotnet/src/Microsoft.Agents.AI.Abstractions/Features/ConversationIdAgentFeature.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ namespace Microsoft.Agents.AI;
88
/// An agent feature that allows providing a conversation identifier.
99
/// </summary>
1010
/// <remarks>
11-
/// This feature allows a user to provide a specific identifier for chat history whether stored in the underlying AI service or stored in a 3rd party store.
11+
/// This feature allows a user to provide a specific identifier for chat history when stored in the underlying AI service.
1212
/// </remarks>
1313
public class ConversationIdAgentFeature
1414
{
1515
/// <summary>
1616
/// Initializes a new instance of the <see cref="ConversationIdAgentFeature"/> class with the specified thread
1717
/// identifier.
1818
/// </summary>
19-
/// <param name="conversationId">The unique identifier of the thread required by the underlying AI service or 3rd party store. Cannot be <see langword="null"/> or empty.</param>
19+
/// <param name="conversationId">The unique identifier of the thread required by the underlying AI service. Cannot be <see langword="null"/> or empty.</param>
2020
public ConversationIdAgentFeature(string conversationId)
2121
{
2222
this.ConversationId = Throw.IfNullOrWhitespace(conversationId);

0 commit comments

Comments
 (0)