forked from microsoft/agent-framework
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathA2AAgent.cs
More file actions
338 lines (277 loc) · 13.5 KB
/
Copy pathA2AAgent.cs
File metadata and controls
338 lines (277 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using A2A;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.A2A;
/// <summary>
/// Represents an <see cref="AIAgent"/> that can interact with remote agents that are exposed via the A2A protocol
/// </summary>
/// <remarks>
/// This agent supports only messages as a response from A2A agents.
/// Support for tasks will be added later as part of the long-running
/// executions work.
/// </remarks>
internal sealed class A2AAgent : AIAgent
{
private readonly A2AClient _a2aClient;
private readonly string? _id;
private readonly string? _name;
private readonly string? _description;
private readonly string? _displayName;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="A2AAgent"/> class.
/// </summary>
/// <param name="a2aClient">The A2A client to use for interacting with A2A agents.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The the name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="displayName">The display name of the agent.</param>
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null)
{
_ = Throw.IfNull(a2aClient);
this._a2aClient = a2aClient;
this._id = id;
this._name = name;
this._description = description;
this._displayName = displayName;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<A2AAgent>();
}
/// <inheritdoc/>
public sealed override AgentThread GetNewThread()
=> new A2AAgentThread();
/// <summary>
/// Get a new <see cref="AgentThread"/> instance using an existing context id, to continue that conversation.
/// </summary>
/// <param name="contextId">The context id to continue.</param>
/// <returns>A new <see cref="AgentThread"/> instance.</returns>
public AgentThread GetNewThread(string contextId)
=> new A2AAgentThread() { ContextId = contextId };
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new A2AAgentThread(serializedThread, jsonSerializerOptions);
/// <inheritdoc/>
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
A2AAgentThread typedThread = this.GetA2AThread(thread, options);
this._logger.LogA2AAgentInvokingAgent(nameof(RunAsync), this.Id, this.Name);
A2AResponse? a2aResponse = null;
if (GetContinuationToken(messages, options) is { } token)
{
a2aResponse = await this._a2aClient.GetTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
else
{
var a2aMessage = CreateA2AMessage(typedThread, messages);
a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
}
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
if (a2aResponse is AgentMessage message)
{
UpdateThread(typedThread, message.ContextId);
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = message.MessageId,
RawRepresentation = message,
Messages = [message.ToChatMessage()],
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
}
if (a2aResponse is AgentTask agentTask)
{
UpdateThread(typedThread, agentTask.ContextId, agentTask.Id);
var response = new AgentRunResponse
{
AgentId = this.Id,
ResponseId = agentTask.Id,
RawRepresentation = agentTask,
Messages = agentTask.ToChatMessages() ?? [],
ContinuationToken = CreateContinuationToken(agentTask.Id, agentTask.Status.State),
AdditionalProperties = agentTask.Metadata?.ToAdditionalProperties(),
};
if (agentTask.ToChatMessages() is { Count: > 0 } taskMessages)
{
response.Messages = taskMessages;
}
return response;
}
throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
}
/// <inheritdoc/>
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
A2AAgentThread typedThread = this.GetA2AThread(thread, options);
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
ConfiguredCancelableAsyncEnumerable<SseItem<A2AEvent>> a2aSseEvents;
if (options?.ContinuationToken is not null)
{
// Task stream resumption is not well defined in the A2A v2.* specification, leaving it to the agent implementations.
// The v3.0 specification improves this by defining task stream reconnection that allows obtaining the same stream
// from the beginning, but it does not define stream resumption from a specific point in the stream.
// Therefore, the code should be updated once the A2A .NET library supports the A2A v3.0 specification,
// and AF has the necessary model to allow consumers to know whether they need to resume the stream and add new updates to
// the existing ones or reconnect the stream and obtain all updates again.
// For more details, see the following issue: https://github.com/microsoft/agent-framework/issues/1764
throw new InvalidOperationException("Reconnecting to task streams using continuation tokens is not supported yet.");
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
var a2aMessage = CreateA2AMessage(typedThread, messages);
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
string? contextId = null;
string? taskId = null;
await foreach (var sseEvent in a2aSseEvents)
{
if (sseEvent.Data is AgentMessage message)
{
contextId = message.ContextId;
yield return this.ConvertToAgentResponseUpdate(message);
}
else if (sseEvent.Data is AgentTask task)
{
contextId = task.ContextId;
taskId = task.Id;
yield return this.ConvertToAgentResponseUpdate(task);
}
else if (sseEvent.Data is TaskUpdateEvent taskUpdateEvent)
{
contextId = taskUpdateEvent.ContextId;
taskId = taskUpdateEvent.TaskId;
yield return this.ConvertToAgentResponseUpdate(taskUpdateEvent);
}
else
{
throw new NotSupportedException($"Only message, task, task update events are supported from A2A agents. Received: {sseEvent.Data.GetType().FullName ?? "null"}");
}
}
UpdateThread(typedThread, contextId, taskId);
}
/// <inheritdoc/>
public override string Id => this._id ?? base.Id;
/// <inheritdoc/>
public override string? Name => this._name ?? base.Name;
/// <inheritdoc/>
public override string DisplayName => this._displayName ?? base.DisplayName;
/// <inheritdoc/>
public override string? Description => this._description ?? base.Description;
private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
{
// Aligning with other agent implementations that support background responses, where
// a thread is required for background responses to prevent inconsistent experience
// for callers if they forget to provide the thread for initial or follow-up runs.
if (options?.AllowBackgroundResponses is true && thread is null)
{
throw new InvalidOperationException("A thread must be provided when AllowBackgroundResponses is enabled.");
}
thread ??= this.GetNewThread();
if (thread is not A2AAgentThread typedThread)
{
throw new InvalidOperationException($"The provided thread type {thread.GetType()} is not compatible with the agent. Only A2A agent created threads are supported.");
}
return typedThread;
}
private static void UpdateThread(A2AAgentThread? thread, string? contextId, string? taskId = null)
{
if (thread is null)
{
return;
}
// Surface cases where the A2A agent responds with a response that
// has a different context Id than the thread's conversation Id.
if (thread.ContextId is not null && contextId is not null && thread.ContextId != contextId)
{
throw new InvalidOperationException(
$"The {nameof(contextId)} returned from the A2A agent is different from the conversation Id of the provided {nameof(AgentThread)}.");
}
// Assign a server-generated context Id to the thread if it's not already set.
thread.ContextId ??= contextId;
thread.TaskId = taskId;
}
private static AgentMessage CreateA2AMessage(A2AAgentThread typedThread, IEnumerable<ChatMessage> messages)
{
var a2aMessage = messages.ToA2AMessage();
// Linking the message to the existing conversation, if any.
// See: https://github.com/a2aproject/A2A/blob/main/docs/topics/life-of-a-task.md#group-related-interactions
a2aMessage.ContextId = typedThread.ContextId;
// Link the message as a follow-up to an existing task, if any.
// See: https://github.com/a2aproject/A2A/blob/main/docs/topics/life-of-a-task.md#task-refinements
a2aMessage.ReferenceTaskIds = typedThread.TaskId is null ? null : [typedThread.TaskId];
return a2aMessage;
}
private static A2AContinuationToken? GetContinuationToken(IEnumerable<ChatMessage> messages, AgentRunOptions? options = null)
{
if (options?.ContinuationToken is ResponseContinuationToken token)
{
if (messages.Any())
{
throw new InvalidOperationException("Messages are not allowed when continuing a background response using a continuation token.");
}
return A2AContinuationToken.FromToken(token);
}
return null;
}
private static A2AContinuationToken? CreateContinuationToken(string taskId, TaskState state)
{
if (state == TaskState.Submitted || state == TaskState.Working)
{
return new A2AContinuationToken(taskId);
}
return null;
}
private AgentRunResponseUpdate ConvertToAgentResponseUpdate(AgentMessage message)
{
return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = message.MessageId,
RawRepresentation = message,
Role = ChatRole.Assistant,
MessageId = message.MessageId,
Contents = message.Parts.ConvertAll(part => part.ToAIContent()),
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
}
private AgentRunResponseUpdate ConvertToAgentResponseUpdate(AgentTask task)
{
return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = task.Id,
RawRepresentation = task,
Role = ChatRole.Assistant,
Contents = task.ToAIContents(),
AdditionalProperties = task.Metadata?.ToAdditionalProperties(),
};
}
private AgentRunResponseUpdate ConvertToAgentResponseUpdate(TaskUpdateEvent taskUpdateEvent)
{
AgentRunResponseUpdate responseUpdate = new()
{
AgentId = this.Id,
ResponseId = taskUpdateEvent.TaskId,
RawRepresentation = taskUpdateEvent,
Role = ChatRole.Assistant,
AdditionalProperties = taskUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
};
if (taskUpdateEvent is TaskArtifactUpdateEvent artifactUpdateEvent)
{
responseUpdate.Contents = artifactUpdateEvent.Artifact.ToAIContents();
responseUpdate.RawRepresentation = artifactUpdateEvent;
}
return responseUpdate;
}
}