mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea370f8ff6 | ||
|
|
24c822590f | ||
|
|
953fde69ac | ||
|
|
7a05849609 | ||
|
|
0aa0579b1b | ||
|
|
844d345106 | ||
|
|
ed5278c41d | ||
|
|
928c9d54ad | ||
|
|
0aba02c402 | ||
|
|
3ef67eff10 | ||
|
|
deea844bc7 | ||
|
|
577ad4b838 | ||
|
|
8b4f7d5e29 |
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://aka.ms/agent-framework
|
||||
about: Check out the official documentation for guides and API reference.
|
||||
- name: Discussions
|
||||
url: https://github.com/microsoft/agent-framework/discussions
|
||||
about: Ask questions and share ideas in GitHub Discussions.
|
||||
@@ -0,0 +1,203 @@
|
||||
name: Issue Report
|
||||
description: Report a bug, request a feature, or ask a question about Microsoft Agent Framework
|
||||
title: "[Issue]: "
|
||||
labels: ["triage"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: language
|
||||
attributes:
|
||||
label: Language
|
||||
description: Which language/SDK are you using?
|
||||
options:
|
||||
- .NET
|
||||
- Python
|
||||
- None / Not Applicable
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Type of Issue
|
||||
description: What type of issue is this?
|
||||
options:
|
||||
- Bug
|
||||
- Feature Request
|
||||
- Question
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Version Information
|
||||
Please provide the version of the package(s) you are using. Select the relevant packages below.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### .NET Packages"
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI
|
||||
description: Version of Microsoft.Agents.AI (e.g., 1.0.0)
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai-abstractions
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI.Abstractions
|
||||
description: Version of Microsoft.Agents.AI.Abstractions
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai-openai
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI.OpenAI
|
||||
description: Version of Microsoft.Agents.AI.OpenAI
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai-azureai
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI.AzureAI
|
||||
description: Version of Microsoft.Agents.AI.AzureAI
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai-anthropic
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI.Anthropic
|
||||
description: Version of Microsoft.Agents.AI.Anthropic
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai-hosting
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI.Hosting
|
||||
description: Version of Microsoft.Agents.AI.Hosting
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-agents-ai-workflows
|
||||
attributes:
|
||||
label: Microsoft.Agents.AI.Workflows
|
||||
description: Version of Microsoft.Agents.AI.Workflows
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-other-packages
|
||||
attributes:
|
||||
label: Other .NET Packages
|
||||
description: List any other Microsoft.Agents.* packages and versions you are using
|
||||
placeholder: "e.g., Microsoft.Agents.AI.CopilotStudio: 1.0.0, Microsoft.Agents.AI.Purview: 1.0.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Python Packages"
|
||||
|
||||
- type: input
|
||||
id: python-core
|
||||
attributes:
|
||||
label: agent-framework-core
|
||||
description: Version of agent-framework-core
|
||||
placeholder: "e.g., 1.0.0b1"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-azure-ai
|
||||
attributes:
|
||||
label: agent-framework-azure-ai
|
||||
description: Version of agent-framework-azure-ai
|
||||
placeholder: "e.g., 1.0.0b1"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-anthropic
|
||||
attributes:
|
||||
label: agent-framework-anthropic
|
||||
description: Version of agent-framework-anthropic
|
||||
placeholder: "e.g., 1.0.0b1"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-azurefunctions
|
||||
attributes:
|
||||
label: agent-framework-azurefunctions
|
||||
description: Version of agent-framework-azurefunctions
|
||||
placeholder: "e.g., 1.0.0b1"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-other-packages
|
||||
attributes:
|
||||
label: Other Python Packages
|
||||
description: List any other agent-framework-* packages and versions you are using
|
||||
placeholder: "e.g., agent-framework-mem0: 1.0.0b1, agent-framework-redis: 1.0.0b1"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "---"
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the issue, feature request, or question.
|
||||
placeholder: |
|
||||
For bugs: Describe what happened, what you expected to happen, and steps to reproduce.
|
||||
For features: Describe the feature you'd like and why it would be useful.
|
||||
For questions: Describe what you're trying to accomplish.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue or your use case.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, screenshots, error messages, or stack traces that might be helpful.
|
||||
placeholder: "Any additional information..."
|
||||
validations:
|
||||
required: false
|
||||
@@ -45,19 +45,58 @@ jobs:
|
||||
labels.push("triage")
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the word 'python' (case-insensitive)
|
||||
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
|
||||
// Add the 'python' label to the array
|
||||
labels.push("python")
|
||||
// Helper function to extract field value from issue form body
|
||||
// Issue forms format fields as: ### Field Name\n\nValue
|
||||
function getFormFieldValue(body, fieldName) {
|
||||
if (!body) return null
|
||||
const regex = new RegExp(`###\\s*${fieldName}\\s*\\n\\n([^\\n#]+)`, 'i')
|
||||
const match = body.match(regex)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
|
||||
if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) ||
|
||||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
|
||||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
|
||||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
|
||||
// Add the '.NET' label to the array
|
||||
labels.push(".NET")
|
||||
// Check for language from issue form dropdown first
|
||||
const languageField = getFormFieldValue(body, 'Language')
|
||||
let languageLabelAdded = false
|
||||
|
||||
if (languageField) {
|
||||
if (languageField === 'Python') {
|
||||
labels.push("python")
|
||||
languageLabelAdded = true
|
||||
} else if (languageField === '.NET') {
|
||||
labels.push(".NET")
|
||||
languageLabelAdded = true
|
||||
}
|
||||
// 'None / Not Applicable' - don't add any language label
|
||||
}
|
||||
|
||||
// Fallback: Check if the body or the title contains the word 'python' (case-insensitive)
|
||||
// Only if language wasn't already determined from the form field
|
||||
if (!languageLabelAdded) {
|
||||
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
|
||||
// Add the 'python' label to the array
|
||||
labels.push("python")
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
|
||||
if ((body != null && body.match(/\.net/i)) || (title != null && title.match(/\.net/i)) ||
|
||||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
|
||||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
|
||||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
|
||||
// Add the '.NET' label to the array
|
||||
labels.push(".NET")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue type from issue form dropdown
|
||||
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
|
||||
if (issueTypeField) {
|
||||
if (issueTypeField === 'Bug') {
|
||||
labels.push("bug")
|
||||
} else if (issueTypeField === 'Feature Request') {
|
||||
labels.push("enhancement")
|
||||
} else if (issueTypeField === 'Question') {
|
||||
labels.push("question")
|
||||
}
|
||||
}
|
||||
|
||||
// Add the labels to the issue (only if there are labels to add)
|
||||
|
||||
@@ -64,7 +64,7 @@ Approaches observed from the compared SDKs:
|
||||
| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
|
||||
| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
|
||||
| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
|
||||
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
|
||||
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
|
||||
| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
|
||||
| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
|
||||
| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
|
||||
@@ -495,8 +495,8 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
| SDK | Structured Outputs support |
|
||||
|-|-|
|
||||
| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
|
||||
| Google ADK | **Approach 1** Both [input and output shemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
|
||||
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.structured_output) |
|
||||
| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
|
||||
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
|
||||
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
|
||||
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
|
||||
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
|
||||
@@ -508,7 +508,7 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
|-|-|
|
||||
| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
|
||||
| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
|
||||
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/api-reference/types/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) class with options that are tied closely to LLM operations. |
|
||||
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
|
||||
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
|
||||
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
|
||||
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
|
||||
|
||||
@@ -19,12 +19,12 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -20,12 +20,12 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -19,12 +19,12 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -96,16 +96,15 @@ public sealed class FunctionTriggers
|
||||
|
||||
// Create a new agent thread
|
||||
AgentThread thread = agentProxy.GetNewThread();
|
||||
AgentThreadMetadata metadata = thread.GetService<AgentThreadMetadata>()
|
||||
?? throw new InvalidOperationException("Failed to get AgentThreadMetadata from new thread.");
|
||||
string agentSessionId = thread.GetService<AgentSessionId>().ToString();
|
||||
|
||||
this._logger.LogInformation("Creating new agent session: {ConversationId}", metadata.ConversationId);
|
||||
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
|
||||
|
||||
// Run the agent in the background (fire-and-forget)
|
||||
DurableAgentRunOptions options = new() { IsFireAndForget = true };
|
||||
await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
|
||||
|
||||
this._logger.LogInformation("Agent run started for session: {ConversationId}", metadata.ConversationId);
|
||||
this._logger.LogInformation("Agent run started for session: {AgentSessionId}", agentSessionId);
|
||||
|
||||
// Check Accept header to determine response format
|
||||
// text/plain = raw text output (ideal for terminals)
|
||||
@@ -114,7 +113,7 @@ public sealed class FunctionTriggers
|
||||
bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
|
||||
|
||||
return await this.StreamToClientAsync(
|
||||
conversationId: metadata.ConversationId!, cursor: null, useSseFormat, request.HttpContext, cancellationToken);
|
||||
conversationId: agentSessionId, cursor: null, useSseFormat, request.HttpContext, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -65,11 +65,10 @@ public sealed class RedisStreamResponseHandler : IAgentResponseHandler
|
||||
"DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
|
||||
}
|
||||
|
||||
// Get conversation ID from the current thread context, which is only available in the context of
|
||||
// Get session ID from the current thread context, which is only available in the context of
|
||||
// a durable agent execution.
|
||||
string conversationId = context.CurrentThread.GetService<AgentThreadMetadata>()?.ConversationId
|
||||
?? throw new InvalidOperationException("Unable to determine conversation ID from the current thread.");
|
||||
string streamKey = GetStreamKey(conversationId);
|
||||
string agentSessionId = context.CurrentThread.GetService<AgentSessionId>().ToString();
|
||||
string streamKey = GetStreamKey(agentSessionId);
|
||||
|
||||
IDatabase db = this._redis.GetDatabase();
|
||||
int sequenceNumber = 0;
|
||||
|
||||
+3
-3
@@ -22,17 +22,17 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -22,17 +22,17 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -35,18 +35,18 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -17,17 +17,17 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+20
-4
@@ -34,7 +34,7 @@ namespace SampleApp
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a thread if the user didn't supply one.
|
||||
thread ??= this.GetNewThread();
|
||||
@@ -44,11 +44,19 @@ namespace SampleApp
|
||||
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
|
||||
}
|
||||
|
||||
// Get existing messages from the store
|
||||
var invokingContext = new ChatMessageStore.InvokingContext(messages);
|
||||
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
|
||||
|
||||
// Clone the input messages and turn them into response messages with upper case text.
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
|
||||
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
|
||||
|
||||
return new AgentRunResponse
|
||||
{
|
||||
@@ -58,7 +66,7 @@ namespace SampleApp
|
||||
};
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a thread if the user didn't supply one.
|
||||
thread ??= this.GetNewThread();
|
||||
@@ -68,11 +76,19 @@ namespace SampleApp
|
||||
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
|
||||
}
|
||||
|
||||
// Get existing messages from the store
|
||||
var invokingContext = new ChatMessageStore.InvokingContext(messages);
|
||||
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
|
||||
|
||||
// Clone the input messages and turn them into response messages with upper case text.
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
|
||||
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
|
||||
|
||||
foreach (var message in responseMessages)
|
||||
{
|
||||
|
||||
+4
-4
@@ -87,10 +87,10 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task<AgentRunResponse> RunAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunAsync(messages, thread, options, cancellationToken);
|
||||
protected sealed override Task<AgentRunResponse> RunCoreAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunStreamingAsync(messages, thread, options, cancellationToken);
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
|
||||
}
|
||||
|
||||
+4
-4
@@ -105,10 +105,10 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunAsync(messages, thread, options, cancellationToken);
|
||||
protected sealed override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunStreamingAsync(messages, thread, options, cancellationToken);
|
||||
protected sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
|
||||
}
|
||||
|
||||
+6
-1
@@ -62,7 +62,12 @@ AIAgent agent = azureOpenAIClient
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
|
||||
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
|
||||
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions),
|
||||
// Since we are using ChatCompletion which stores chat history locally, we can also add a message removal policy
|
||||
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
|
||||
// we don't bloat chat history with all the search result messages.
|
||||
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions)
|
||||
.WithAIContextProviderMessageRemoval(),
|
||||
});
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
+28
-18
@@ -89,24 +89,7 @@ namespace SampleApp
|
||||
|
||||
public string? ThreadDbKey { get; private set; }
|
||||
|
||||
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
|
||||
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem()
|
||||
{
|
||||
Key = this.ThreadDbKey + x.MessageId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
ThreadId = this.ThreadDbKey,
|
||||
SerializedMessage = JsonSerializer.Serialize(x),
|
||||
MessageText = x.Text
|
||||
}), cancellationToken);
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
|
||||
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
@@ -124,6 +107,33 @@ namespace SampleApp
|
||||
return messages;
|
||||
}
|
||||
|
||||
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Don't store messages if the request failed.
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
|
||||
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
// Add both request and response messages to the store
|
||||
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
|
||||
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
|
||||
|
||||
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
|
||||
{
|
||||
Key = this.ThreadDbKey + x.MessageId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
ThreadId = this.ThreadDbKey,
|
||||
SerializedMessage = JsonSerializer.Serialize(x),
|
||||
MessageText = x.Text
|
||||
}), cancellationToken);
|
||||
}
|
||||
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
|
||||
// We have to serialize the thread id, so that on deserialization we can retrieve the messages using the same thread id.
|
||||
JsonSerializer.SerializeToElement(this.ThreadDbKey);
|
||||
|
||||
@@ -48,9 +48,9 @@ public class WeatherForecastAgent : DelegatingAIAgent
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await base.RunAsync(messages, thread, options, cancellationToken);
|
||||
var response = await base.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
// If the agent returned a valid structured output response
|
||||
// we might be able to enhance the response with an adaptive card.
|
||||
|
||||
@@ -68,7 +68,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
=> new A2AAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
@@ -131,7 +131,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
|
||||
@@ -218,6 +218,35 @@ public abstract class AIAgent
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method delegates to <see cref="RunCoreAsync"/> to perform the actual agent invocation. It handles collections of messages,
|
||||
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
|
||||
/// context-rich conversations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The messages are processed in the order provided and become part of the conversation history.
|
||||
/// The agent's response will also be added to <paramref name="thread"/> if one is provided.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of the agent invocation logic with a collection of chat messages.
|
||||
/// </summary>
|
||||
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
|
||||
/// <param name="thread">
|
||||
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
|
||||
/// The thread will be updated with the input messages and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the primary invocation method that implementations must override. It handles collections of messages,
|
||||
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
|
||||
/// context-rich conversations.
|
||||
@@ -227,7 +256,7 @@ public abstract class AIAgent
|
||||
/// The agent's response will also be added to <paramref name="thread"/> if one is provided.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
protected abstract Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -312,6 +341,34 @@ public abstract class AIAgent
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method delegates to <see cref="RunCoreStreamingAsync"/> to perform the actual streaming invocation. It provides real-time
|
||||
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each <see cref="AgentRunResponseUpdate"/> represents a portion of the complete response, allowing consumers
|
||||
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
|
||||
/// </summary>
|
||||
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
|
||||
/// <param name="thread">
|
||||
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
|
||||
/// The thread will be updated with the input messages and any response updates generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the primary streaming invocation method that implementations must override. It provides real-time
|
||||
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
|
||||
/// </para>
|
||||
@@ -320,7 +377,7 @@ public abstract class AIAgent
|
||||
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected abstract IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -68,7 +68,7 @@ public abstract class AgentThread
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AgentThread"/>,
|
||||
/// including itself or any services it might be wrapping. For example, to access the <see cref="AgentThreadMetadata"/> for the instance,
|
||||
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatMessageStore"/> if available for the instance,
|
||||
/// <see cref="GetService"/> may be used to request it.
|
||||
/// </remarks>
|
||||
public virtual object? GetService(Type serviceType, object? serviceKey = null)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides metadata information about an <see cref="AgentThread"/> instance.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("ConversationId = {ConversationId}")]
|
||||
public class AgentThreadMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentThreadMetadata"/> class.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The unique identifier for the conversation, if available.</param>
|
||||
public AgentThreadMetadata(string? conversationId)
|
||||
{
|
||||
this.ConversationId = conversationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for the conversation, if available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The meaning of this ID may vary depending on the agent implementation.
|
||||
/// </remarks>
|
||||
public string? ConversationId { get; }
|
||||
}
|
||||
@@ -32,8 +32,9 @@ namespace Microsoft.Agents.AI;
|
||||
public abstract class ChatMessageStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves all messages from the store that should be provided as context for the next agent invocation.
|
||||
/// Called at the start of agent invocation to retrieve all messages from the store that should be provided as context for the next agent invocation.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous operation. The task result contains a collection of <see cref="ChatMessage"/>
|
||||
@@ -59,20 +60,19 @@ public abstract class ChatMessageStore
|
||||
/// and context management.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default);
|
||||
public abstract ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously adds new messages to the store.
|
||||
/// Called at the end of the agent invocation to add new messages to the store.
|
||||
/// </summary>
|
||||
/// <param name="messages">The collection of chat messages to add to the store.</param>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous add operation.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
|
||||
/// The store is responsible for preserving message ordering and ensuring that subsequent calls to
|
||||
/// <see cref="GetMessagesAsync"/> return messages in the correct chronological order.
|
||||
/// <see cref="InvokingAsync"/> return messages in the correct chronological order.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations may perform additional processing during message addition, such as:
|
||||
@@ -83,8 +83,12 @@ public abstract class ChatMessageStore
|
||||
/// <item><description>Updating indices or search capabilities</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is called regardless of whether the invocation succeeded or failed.
|
||||
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default);
|
||||
public abstract ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
|
||||
@@ -121,4 +125,100 @@ public abstract class ChatMessageStore
|
||||
/// </remarks>
|
||||
public TService? GetService<TService>(object? serviceKey = null)
|
||||
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about the invocation before the messages are retrieved from the store,
|
||||
/// including the new messages that will be used. Stores can use this information to determine what
|
||||
/// messages should be retrieved for the invocation.
|
||||
/// </remarks>
|
||||
public sealed class InvokingContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
|
||||
/// </summary>
|
||||
/// <param name="requestMessages">The new messages to be used by the agent for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
|
||||
public InvokingContext(IEnumerable<ChatMessage> requestMessages)
|
||||
{
|
||||
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the caller provided messages that will be used by the agent for this invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokedAsync(InvokedContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about a completed agent invocation, including both the
|
||||
/// request messages that were used and the response messages that were generated. It also indicates
|
||||
/// whether the invocation succeeded or failed.
|
||||
/// </remarks>
|
||||
public sealed class InvokedContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
|
||||
/// </summary>
|
||||
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
|
||||
/// <param name="chatMessageStoreMessages">The messages retrieved from the <see cref="ChatMessageStore"/> for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
|
||||
public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage> chatMessageStoreMessages)
|
||||
{
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
this.ChatMessageStoreMessages = chatMessageStoreMessages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the caller provided messages that were used by the agent for this invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
|
||||
/// This does not include any <see cref="ChatMessageStore"/> supplied messages.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages retrieved from the <see cref="ChatMessageStore"/> for this invocation, if any.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances that were retrieved from the <see cref="ChatMessageStore"/>,
|
||||
/// and were used by the agent as part of the invocation.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage> ChatMessageStoreMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances that were provided by the <see cref="AIContextProvider"/>,
|
||||
/// and were used by the agent as part of the invocation.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage>? AIContextProviderMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing the response,
|
||||
/// or <see langword="null"/> if the invocation failed or did not produce response messages.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage>? ResponseMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The exception that caused the invocation to fail, or <see langword="null"/> if the invocation succeeded.
|
||||
/// </value>
|
||||
public Exception? InvokeException { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for the <see cref="ChatMessageStore"/> class.
|
||||
/// </summary>
|
||||
public static class ChatMessageStoreExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds message filtering to an existing store, so that messages passed to the store and messages produced by the store
|
||||
/// can be filtered, updated or replaced.
|
||||
/// </summary>
|
||||
/// <param name="store">The store to add the message filter to.</param>
|
||||
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages produced by the store. If null, no filter is applied at this
|
||||
/// stage.</param>
|
||||
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invoked context messages before they are passed to the store. If null, no
|
||||
/// filter is applied at this stage.</param>
|
||||
/// <returns>The <see cref="ChatMessageStore"/> with filtering applied.</returns>
|
||||
public static ChatMessageStore WithMessageFilters(
|
||||
this ChatMessageStore store,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
|
||||
Func<ChatMessageStore.InvokedContext, ChatMessageStore.InvokedContext>? invokedMessagesFilter = null)
|
||||
{
|
||||
return new ChatMessageStoreMessageFilter(
|
||||
innerChatMessageStore: store,
|
||||
invokingMessagesFilter: invokingMessagesFilter,
|
||||
invokedMessagesFilter: invokedMessagesFilter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decorates the provided chat message store so that it does not store messages produced by any <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="store">The store to add the message filter to.</param>
|
||||
/// <returns>A new <see cref="ChatMessageStore"/> instance that filters out <see cref="AIContextProvider"/> messages so they do not get stored.</returns>
|
||||
public static ChatMessageStore WithAIContextProviderMessageRemoval(this ChatMessageStore store)
|
||||
{
|
||||
return new ChatMessageStoreMessageFilter(
|
||||
innerChatMessageStore: store,
|
||||
invokedMessagesFilter: (ctx) =>
|
||||
{
|
||||
ctx.AIContextProviderMessages = null;
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="ChatMessageStore"/> decorator that allows filtering the messages
|
||||
/// passed into and out of an inner <see cref="ChatMessageStore"/>.
|
||||
/// </summary>
|
||||
public sealed class ChatMessageStoreMessageFilter : ChatMessageStore
|
||||
{
|
||||
private readonly ChatMessageStore _innerChatMessageStore;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _invokingMessagesFilter;
|
||||
private readonly Func<InvokedContext, InvokedContext>? _invokedMessagesFilter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatMessageStoreMessageFilter"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>Use this constructor to customize how messages are filtered before and after invocation by
|
||||
/// providing appropriate filter functions. If no filters are provided, the message store operates without
|
||||
/// additional filtering.</remarks>
|
||||
/// <param name="innerChatMessageStore">The underlying chat message store to be wrapped. Cannot be null.</param>
|
||||
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages before they are invoked. If null, no filter is applied at this
|
||||
/// stage.</param>
|
||||
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invocation context after messages have been invoked. If null, no
|
||||
/// filter is applied at this stage.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if innerChatMessageStore is null.</exception>
|
||||
public ChatMessageStoreMessageFilter(
|
||||
ChatMessageStore innerChatMessageStore,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
|
||||
Func<InvokedContext, InvokedContext>? invokedMessagesFilter = null)
|
||||
{
|
||||
this._innerChatMessageStore = Throw.IfNull(innerChatMessageStore);
|
||||
|
||||
if (invokingMessagesFilter == null && invokedMessagesFilter == null)
|
||||
{
|
||||
throw new ArgumentException("At least one filter function, invokingMessagesFilter or invokedMessagesFilter, must be provided.");
|
||||
}
|
||||
|
||||
this._invokingMessagesFilter = invokingMessagesFilter;
|
||||
this._invokedMessagesFilter = invokedMessagesFilter;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var messages = await this._innerChatMessageStore.InvokingAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
return this._invokingMessagesFilter != null ? this._invokingMessagesFilter(messages) : messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._invokedMessagesFilter != null)
|
||||
{
|
||||
context = this._invokedMessagesFilter(context);
|
||||
}
|
||||
|
||||
return this._innerChatMessageStore.InvokedAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
return this._innerChatMessageStore.Serialize(jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ public abstract class DelegatingAIAgent : AIAgent
|
||||
=> this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -89,7 +89,7 @@ public abstract class DelegatingAIAgent : AIAgent
|
||||
=> this.InnerAgent.RunAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -134,21 +134,10 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
this._messages.AddRange(messages);
|
||||
|
||||
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
{
|
||||
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
|
||||
{
|
||||
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
@@ -157,6 +146,26 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
|
||||
return this._messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add request, AI context provider, and response messages to the store
|
||||
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
|
||||
this._messages.AddRange(allNewMessages);
|
||||
|
||||
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
{
|
||||
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
@@ -221,7 +230,7 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger the reducer when a new message is added.
|
||||
/// <see cref="AddMessagesAsync(IEnumerable{ChatMessage}, CancellationToken)"/> will only complete when reducer processing is done.
|
||||
/// <see cref="InvokedAsync(InvokedContext, CancellationToken)"/> will only complete when reducer processing is done.
|
||||
/// </summary>
|
||||
AfterMessageAdded,
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
=> new CopilotStudioAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -96,7 +96,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -287,7 +287,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
|
||||
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
|
||||
if (this._disposed)
|
||||
@@ -347,11 +347,14 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (messages is null)
|
||||
Throw.IfNull(context);
|
||||
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(messages));
|
||||
// Do not store messages if there was an exception during invocation
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
|
||||
@@ -361,7 +364,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
|
||||
}
|
||||
#pragma warning restore CA1513
|
||||
|
||||
var messageList = messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
var messageList = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []).ToList();
|
||||
if (messageList.Count == 0)
|
||||
{
|
||||
return;
|
||||
@@ -381,7 +384,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
|
||||
/// <summary>
|
||||
/// Adds multiple messages using transactional batch operations for atomicity.
|
||||
/// </summary>
|
||||
private async Task AddMessagesInBatchAsync(IReadOnlyCollection<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
private async Task AddMessagesInBatchAsync(List<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
|
||||
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
|
||||
- Removed AgentThreadMetadata and used AgentSessionId directly instead ([#3067](https://github.com/microsoft/agent-framework/pull/3067));
|
||||
|
||||
## v1.0.0-preview.251204.1
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed class DurableAIAgent : AIAgent
|
||||
/// <exception cref="AgentNotRegisteredException">Thrown when the agent has not been registered.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the provided thread is not valid for a durable agent.</exception>
|
||||
/// <exception cref="NotSupportedException">Thrown when cancellation is requested (cancellation is not supported for durable agents).</exception>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -128,7 +128,7 @@ public sealed class DurableAIAgent : AIAgent
|
||||
/// <param name="options">Optional run options.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A streaming response enumerable.</returns>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
|
||||
return new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!));
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -70,7 +70,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
|
||||
return await agentRunHandle.ReadAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -55,12 +55,6 @@ public sealed class DurableAgentThread : AgentThread
|
||||
/// <inheritdoc/>
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
// This is a common convention for MAF agents.
|
||||
if (serviceType == typeof(AgentThreadMetadata))
|
||||
{
|
||||
return new AgentThreadMetadata(conversationId: this.SessionId.ToString());
|
||||
}
|
||||
|
||||
if (serviceType == typeof(AgentSessionId))
|
||||
{
|
||||
return this.SessionId;
|
||||
|
||||
@@ -21,13 +21,13 @@ internal sealed class EntityAgentWrapper(
|
||||
// The ID of the agent is always the entity ID.
|
||||
protected override string? IdCore => this._entityContext.Id.ToString();
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentRunResponse response = await base.RunAsync(
|
||||
AgentRunResponse response = await base.RunCoreAsync(
|
||||
messages,
|
||||
thread,
|
||||
this.GetAgentEntityRunOptions(options),
|
||||
@@ -37,13 +37,13 @@ internal sealed class EntityAgentWrapper(
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await foreach (AgentRunResponseUpdate update in base.RunStreamingAsync(
|
||||
await foreach (AgentRunResponseUpdate update in base.RunCoreStreamingAsync(
|
||||
messages,
|
||||
thread,
|
||||
this.GetAgentEntityRunOptions(options),
|
||||
|
||||
@@ -42,13 +42,13 @@ internal class PurviewAgent : AIAgent, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._purviewWrapper.ProcessAgentContentAsync(messages, thread, options, this._innerAgent, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await this._purviewWrapper.ProcessAgentContentAsync(messages, thread, options, this._innerAgent, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var update in response.ToAgentRunResponseUpdates())
|
||||
|
||||
@@ -66,7 +66,7 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, jsonSerializerOptions);
|
||||
|
||||
private async ValueTask<WorkflowThread> UpdateThreadAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, CancellationToken cancellationToken = default)
|
||||
private ValueTask<WorkflowThread> UpdateThreadAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
thread ??= this.GetNewThread();
|
||||
|
||||
@@ -75,12 +75,14 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
throw new ArgumentException($"Incompatible thread type: {thread.GetType()} (expecting {typeof(WorkflowThread)})", nameof(thread));
|
||||
}
|
||||
|
||||
await workflowThread.MessageStore.AddMessagesAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
return workflowThread;
|
||||
// For workflow threads, messages are added directly via the internal AddMessages method
|
||||
// The MessageStore methods are used for agent invocation scenarios
|
||||
workflowThread.MessageStore.AddMessages(messages);
|
||||
return new ValueTask<WorkflowThread>(workflowThread);
|
||||
}
|
||||
|
||||
public override async
|
||||
Task<AgentRunResponse> RunAsync(
|
||||
protected override async
|
||||
Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -101,8 +103,8 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
return merger.ComputeMerged(workflowThread.LastResponseId!, this.Id, this.Name);
|
||||
}
|
||||
|
||||
public override async
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -45,15 +46,22 @@ internal sealed class WorkflowMessageStore : ChatMessageStore
|
||||
|
||||
internal void AddMessages(params IEnumerable<ChatMessage> messages) => this._chatMessages.AddRange(messages);
|
||||
|
||||
public override Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> new(this._chatMessages.AsReadOnly());
|
||||
|
||||
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._chatMessages.AddRange(messages);
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
|
||||
this._chatMessages.AddRange(allNewMessages);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default) => Task.FromResult<IEnumerable<ChatMessage>>(this._chatMessages.AsReadOnly());
|
||||
|
||||
public IEnumerable<ChatMessage> GetFromBookmark()
|
||||
{
|
||||
for (int i = this._bookmark; i < this._chatMessages.Count; i++)
|
||||
|
||||
@@ -17,18 +17,18 @@ namespace Microsoft.Agents.AI;
|
||||
/// </remarks>
|
||||
internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
|
||||
{
|
||||
/// <summary>The delegate to use as the implementation of <see cref="RunAsync"/>.</summary>
|
||||
/// <summary>The delegate to use as the implementation of <see cref="RunCoreAsync"/>.</summary>
|
||||
private readonly Func<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, AIAgent, CancellationToken, Task<AgentRunResponse>>? _runFunc;
|
||||
|
||||
/// <summary>The delegate to use as the implementation of <see cref="RunStreamingAsync"/>.</summary>
|
||||
/// <summary>The delegate to use as the implementation of <see cref="RunCoreStreamingAsync"/>.</summary>
|
||||
/// <remarks>
|
||||
/// When non-<see langword="null"/>, this delegate is used as the implementation of <see cref="RunStreamingAsync"/> and
|
||||
/// When non-<see langword="null"/>, this delegate is used as the implementation of <see cref="RunCoreStreamingAsync"/> and
|
||||
/// will be invoked with the same arguments as the method itself.
|
||||
/// When <see langword="null"/>, <see cref="RunStreamingAsync"/> will delegate directly to the inner agent.
|
||||
/// When <see langword="null"/>, <see cref="RunCoreStreamingAsync"/> will delegate directly to the inner agent.
|
||||
/// </remarks>
|
||||
private readonly Func<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, AIAgent, CancellationToken, IAsyncEnumerable<AgentRunResponseUpdate>>? _runStreamingFunc;
|
||||
|
||||
/// <summary>The delegate to use as the implementation of both <see cref="RunAsync"/> and <see cref="RunStreamingAsync"/>.</summary>
|
||||
/// <summary>The delegate to use as the implementation of both <see cref="RunCoreAsync"/> and <see cref="RunCoreStreamingAsync"/>.</summary>
|
||||
private readonly Func<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, Func<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, CancellationToken, Task>, CancellationToken, Task>? _sharedFunc;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,7 +36,7 @@ internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The inner agent.</param>
|
||||
/// <param name="sharedFunc">
|
||||
/// A delegate that provides the implementation for both <see cref="RunAsync"/> and <see cref="RunStreamingAsync"/>.
|
||||
/// A delegate that provides the implementation for both <see cref="RunCoreAsync"/> and <see cref="RunCoreStreamingAsync"/>.
|
||||
/// In addition to the arguments for the operation, it's provided with a delegate to the inner agent that should be
|
||||
/// used to perform the operation on the inner agent. It will handle both the non-streaming and streaming cases.
|
||||
/// </param>
|
||||
@@ -61,13 +61,13 @@ internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The inner agent.</param>
|
||||
/// <param name="runFunc">
|
||||
/// A delegate that provides the implementation for <see cref="RunAsync"/>. When <see langword="null"/>,
|
||||
/// <paramref name="runStreamingFunc"/> must be non-null, and the implementation of <see cref="RunAsync"/>
|
||||
/// A delegate that provides the implementation for <see cref="RunCoreAsync"/>. When <see langword="null"/>,
|
||||
/// <paramref name="runStreamingFunc"/> must be non-null, and the implementation of <see cref="RunCoreAsync"/>
|
||||
/// will use <paramref name="runStreamingFunc"/> for the implementation.
|
||||
/// </param>
|
||||
/// <param name="runStreamingFunc">
|
||||
/// A delegate that provides the implementation for <see cref="RunStreamingAsync"/>. When <see langword="null"/>,
|
||||
/// <paramref name="runFunc"/> must be non-null, and the implementation of <see cref="RunStreamingAsync"/>
|
||||
/// A delegate that provides the implementation for <see cref="RunCoreStreamingAsync"/>. When <see langword="null"/>,
|
||||
/// <paramref name="runFunc"/> must be non-null, and the implementation of <see cref="RunCoreStreamingAsync"/>
|
||||
/// will use <paramref name="runFunc"/> for the implementation.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
|
||||
@@ -85,7 +85,7 @@ internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -132,7 +132,7 @@ internal sealed class AnonymousDelegatingAIAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -149,7 +149,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
internal ChatOptions? ChatOptions => this._agentOptions?.ChatOptions;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -193,7 +193,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -201,7 +201,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
{
|
||||
var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
|
||||
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> inputMessagesForChatClient, IList<ChatMessage>? aiContextProviderMessages) =
|
||||
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> inputMessagesForChatClient, IList<ChatMessage>? aiContextProviderMessages, IList<ChatMessage>? chatMessageStoreMessages) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ValidateStreamResumptionAllowed(chatOptions?.ContinuationToken, safeThread);
|
||||
@@ -225,6 +225,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
@@ -239,6 +240,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
@@ -260,6 +262,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
@@ -272,7 +275,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
|
||||
|
||||
// To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request.
|
||||
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
@@ -379,7 +382,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
{
|
||||
var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
|
||||
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> inputMessagesForChatClient, IList<ChatMessage>? aiContextProviderMessages) =
|
||||
(ChatClientAgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> inputMessagesForChatClient, IList<ChatMessage>? aiContextProviderMessages, IList<ChatMessage>? chatMessageStoreMessages) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var chatClient = this.ChatClient;
|
||||
@@ -398,6 +401,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
@@ -415,7 +419,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
|
||||
// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent message state in the thread.
|
||||
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages, chatMessageStoreMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
@@ -603,7 +607,14 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// <param name="runOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A tuple containing the thread, chat options, and thread messages.</returns>
|
||||
private async Task<(ChatClientAgentThread AgentThread, ChatOptions? ChatOptions, List<ChatMessage> InputMessagesForChatClient, IList<ChatMessage>? AIContextProviderMessages)> PrepareThreadAndMessagesAsync(
|
||||
private async Task
|
||||
<(
|
||||
ChatClientAgentThread AgentThread,
|
||||
ChatOptions? ChatOptions,
|
||||
List<ChatMessage> InputMessagesForChatClient,
|
||||
IList<ChatMessage>? AIContextProviderMessages,
|
||||
IList<ChatMessage>? ChatMessageStoreMessages
|
||||
)> PrepareThreadAndMessagesAsync(
|
||||
AgentThread? thread,
|
||||
IEnumerable<ChatMessage> inputMessages,
|
||||
AgentRunOptions? runOptions,
|
||||
@@ -637,6 +648,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
|
||||
List<ChatMessage> inputMessagesForChatClient = [];
|
||||
IList<ChatMessage>? aiContextProviderMessages = null;
|
||||
IList<ChatMessage>? chatMessageStoreMessages = null;
|
||||
|
||||
// Populate the thread messages only if we are not continuing an existing response as it's not allowed
|
||||
if (chatOptions?.ContinuationToken is null)
|
||||
@@ -644,9 +656,15 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
// Add any existing messages from the thread to the messages to be sent to the chat client.
|
||||
if (typedThread.MessageStore is not null)
|
||||
{
|
||||
inputMessagesForChatClient.AddRange(await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
|
||||
var invokingContext = new ChatMessageStore.InvokingContext(inputMessages);
|
||||
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
|
||||
inputMessagesForChatClient.AddRange(storeMessages);
|
||||
chatMessageStoreMessages = storeMessages as IList<ChatMessage> ?? storeMessages.ToList();
|
||||
}
|
||||
|
||||
// Add the input messages before getting context from AIContextProvider.
|
||||
inputMessagesForChatClient.AddRange(inputMessages);
|
||||
|
||||
// If we have an AIContextProvider, we should get context from it, and update our
|
||||
// messages and options with the additional context.
|
||||
if (typedThread.AIContextProvider is not null)
|
||||
@@ -675,9 +693,6 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
|
||||
}
|
||||
}
|
||||
|
||||
// Add the input messages to the end of thread messages.
|
||||
inputMessagesForChatClient.AddRange(inputMessages);
|
||||
}
|
||||
|
||||
// If a user provided two different thread ids, via the thread object and options, we should throw
|
||||
@@ -698,7 +713,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
chatOptions.ConversationId = typedThread.ConversationId;
|
||||
}
|
||||
|
||||
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages);
|
||||
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages, chatMessageStoreMessages);
|
||||
}
|
||||
|
||||
private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread thread, string? responseConversationId)
|
||||
@@ -725,7 +740,13 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
private static Task NotifyMessageStoreOfNewMessagesAsync(ChatClientAgentThread thread, IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken)
|
||||
private static Task NotifyMessageStoreOfFailureAsync(
|
||||
ChatClientAgentThread thread,
|
||||
Exception ex,
|
||||
IEnumerable<ChatMessage> requestMessages,
|
||||
IEnumerable<ChatMessage>? chatMessageStoreMessages,
|
||||
IEnumerable<ChatMessage>? aiContextProviderMessages,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var messageStore = thread.MessageStore;
|
||||
|
||||
@@ -733,7 +754,38 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
|
||||
if (messageStore is not null)
|
||||
{
|
||||
return messageStore.AddMessagesAsync(newMessages, cancellationToken);
|
||||
var invokedContext = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages!)
|
||||
{
|
||||
AIContextProviderMessages = aiContextProviderMessages,
|
||||
InvokeException = ex
|
||||
};
|
||||
|
||||
return messageStore.InvokedAsync(invokedContext, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task NotifyMessageStoreOfNewMessagesAsync(
|
||||
ChatClientAgentThread thread,
|
||||
IEnumerable<ChatMessage> requestMessages,
|
||||
IEnumerable<ChatMessage>? chatMessageStoreMessages,
|
||||
IEnumerable<ChatMessage>? aiContextProviderMessages,
|
||||
IEnumerable<ChatMessage> responseMessages,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var messageStore = thread.MessageStore;
|
||||
|
||||
// Only notify the message store if we have one.
|
||||
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
|
||||
if (messageStore is not null)
|
||||
{
|
||||
var invokedContext = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages!)
|
||||
{
|
||||
AIContextProviderMessages = aiContextProviderMessages,
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
return messageStore.InvokedAsync(invokedContext, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// Provides a thread implementation for use with <see cref="ChatClientAgent"/>.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public class ChatClientAgentThread : AgentThread
|
||||
public sealed class ChatClientAgentThread : AgentThread
|
||||
{
|
||||
private ChatMessageStore? _messageStore;
|
||||
|
||||
@@ -171,9 +171,7 @@ public class ChatClientAgentThread : AgentThread
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
serviceType == typeof(AgentThreadMetadata)
|
||||
? new AgentThreadMetadata(this.ConversationId)
|
||||
: base.GetService(serviceType, serviceKey)
|
||||
base.GetService(serviceType, serviceKey)
|
||||
?? this.AIContextProvider?.GetService(serviceType, serviceKey)
|
||||
?? this.MessageStore?.GetService(serviceType, serviceKey);
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ internal sealed class FunctionInvocationDelegatingAgent : DelegatingAIAgent
|
||||
this._delegateFunc = delegateFunc;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> this.InnerAgent.RunAsync(messages, thread, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken);
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> this.InnerAgent.RunStreamingAsync(messages, thread, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken);
|
||||
|
||||
// Decorate options to add the middleware function
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed partial class LoggingAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
@@ -72,7 +72,7 @@ public sealed partial class LoggingAgent : DelegatingAIAgent
|
||||
|
||||
try
|
||||
{
|
||||
AgentRunResponse response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
AgentRunResponse response = await base.RunCoreAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
@@ -101,7 +101,7 @@ public sealed partial class LoggingAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
@@ -119,7 +119,7 @@ public sealed partial class LoggingAgent : DelegatingAIAgent
|
||||
IAsyncEnumerator<AgentRunResponseUpdate> e;
|
||||
try
|
||||
{
|
||||
e = base.RunStreamingAsync(messages, thread, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
|
||||
e = base.RunCoreStreamingAsync(messages, thread, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
|
||||
@@ -89,7 +89,7 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
|
||||
|
||||
+6
-1
@@ -39,7 +39,12 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture
|
||||
{
|
||||
var typedThread = (ChatClientAgentThread)thread;
|
||||
|
||||
return typedThread.MessageStore is null ? [] : (await typedThread.MessageStore.GetMessagesAsync()).ToList();
|
||||
if (typedThread.MessageStore is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await typedThread.MessageStore.InvokingAsync(new([]))).ToList();
|
||||
}
|
||||
|
||||
public Task<ChatClientAgent> CreateChatClientAgentAsync(
|
||||
|
||||
@@ -48,7 +48,12 @@ public class AIProjectClientFixture : IChatClientAgentFixture
|
||||
return await this.GetChatHistoryFromResponsesChainAsync(chatClientThread.ConversationId);
|
||||
}
|
||||
|
||||
return chatClientThread.MessageStore is null ? [] : (await chatClientThread.MessageStore.GetMessagesAsync()).ToList();
|
||||
if (chatClientThread.MessageStore is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await chatClientThread.MessageStore.InvokingAsync(new([]))).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<ChatMessage>> GetChatHistoryFromResponsesChainAsync(string conversationId)
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
@@ -33,18 +34,20 @@ public class AIAgentTests
|
||||
|
||||
this._agentMock = new Mock<AIAgent> { CallBase = true };
|
||||
this._agentMock
|
||||
.Setup(x => x.RunAsync(
|
||||
It.IsAny<IReadOnlyCollection<ChatMessage>>(),
|
||||
this._agentThreadMock.Object,
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(this._invokeResponse);
|
||||
this._agentMock
|
||||
.Setup(x => x.RunStreamingAsync(
|
||||
It.IsAny<IReadOnlyCollection<ChatMessage>>(),
|
||||
this._agentThreadMock.Object,
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Protected()
|
||||
.Setup<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerableAsync(this._invokeStreamingResponses));
|
||||
}
|
||||
|
||||
@@ -64,13 +67,14 @@ public class AIAgentTests
|
||||
Assert.Equal(this._invokeResponse, response);
|
||||
|
||||
// Verify that the mocked method was called with the expected parameters
|
||||
this._agentMock.Verify(
|
||||
x => x.RunAsync(
|
||||
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 0),
|
||||
this._agentThreadMock.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
this._agentMock
|
||||
.Protected()
|
||||
.Verify<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(messages => !messages.Any()),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == options),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,13 +94,14 @@ public class AIAgentTests
|
||||
Assert.Equal(this._invokeResponse, response);
|
||||
|
||||
// Verify that the mocked method was called with the expected parameters
|
||||
this._agentMock.Verify(
|
||||
x => x.RunAsync(
|
||||
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First().Text == Message),
|
||||
this._agentThreadMock.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
this._agentMock
|
||||
.Protected()
|
||||
.Verify<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First().Text == Message),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == options),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -116,13 +121,14 @@ public class AIAgentTests
|
||||
Assert.Equal(this._invokeResponse, response);
|
||||
|
||||
// Verify that the mocked method was called with the expected parameters
|
||||
this._agentMock.Verify(
|
||||
x => x.RunAsync(
|
||||
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First() == message),
|
||||
this._agentThreadMock.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
this._agentMock
|
||||
.Protected()
|
||||
.Verify<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First() == message),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == options),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,13 +150,14 @@ public class AIAgentTests
|
||||
}
|
||||
|
||||
// Verify that the mocked method was called with the expected parameters
|
||||
this._agentMock.Verify(
|
||||
x => x.RunStreamingAsync(
|
||||
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 0),
|
||||
this._agentThreadMock.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
this._agentMock
|
||||
.Protected()
|
||||
.Verify<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(messages => !messages.Any()),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == options),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -173,13 +180,14 @@ public class AIAgentTests
|
||||
}
|
||||
|
||||
// Verify that the mocked method was called with the expected parameters
|
||||
this._agentMock.Verify(
|
||||
x => x.RunStreamingAsync(
|
||||
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First().Text == Message),
|
||||
this._agentThreadMock.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
this._agentMock
|
||||
.Protected()
|
||||
.Verify<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First().Text == Message),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == options),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -202,13 +210,14 @@ public class AIAgentTests
|
||||
}
|
||||
|
||||
// Verify that the mocked method was called with the expected parameters
|
||||
this._agentMock.Verify(
|
||||
x => x.RunStreamingAsync(
|
||||
It.Is<IReadOnlyCollection<ChatMessage>>(messages => messages.Count == 1 && messages.First() == message),
|
||||
this._agentThreadMock.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
this._agentMock
|
||||
.Protected()
|
||||
.Verify<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First() == message),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.Object),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == options),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -375,14 +384,14 @@ public class AIAgentTests
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="ChatMessageStoreMessageFilter"/> class.
|
||||
/// </summary>
|
||||
public sealed class ChatMessageStoreMessageFilterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithNullInnerStore_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new ChatMessageStoreMessageFilter(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithOnlyInnerStore_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => new ChatMessageStoreMessageFilter(innerStoreMock.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithAllParameters_CreatesInstance()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
|
||||
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) => msgs;
|
||||
ChatMessageStore.InvokedContext InvokedFilter(ChatMessageStore.InvokedContext ctx) => ctx;
|
||||
|
||||
// Act
|
||||
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter, InvokedFilter);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(filter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_WithNoOpFilters_ReturnsInnerStoreMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
var expectedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
innerStoreMock
|
||||
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedMessages);
|
||||
|
||||
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, x => x, x => x);
|
||||
|
||||
// Act
|
||||
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("Hello", result[0].Text);
|
||||
Assert.Equal("Hi there!", result[1].Text);
|
||||
innerStoreMock.Verify(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_WithInvokingFilter_AppliesFilterAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
var innerMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!"),
|
||||
new(ChatRole.User, "How are you?")
|
||||
};
|
||||
var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
innerStoreMock
|
||||
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(innerMessages);
|
||||
|
||||
// Filter to only user messages
|
||||
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) => msgs.Where(m => m.Role == ChatRole.User);
|
||||
|
||||
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter);
|
||||
|
||||
// Act
|
||||
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role));
|
||||
innerStoreMock.Verify(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_WithInvokingFilter_CanModifyMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
var innerMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
innerStoreMock
|
||||
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(innerMessages);
|
||||
|
||||
// Filter that transforms messages
|
||||
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) =>
|
||||
msgs.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}"));
|
||||
|
||||
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter);
|
||||
|
||||
// Act
|
||||
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("[FILTERED] Hello", result[0].Text);
|
||||
Assert.Equal("[FILTERED] Hi there!", result[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_WithInvokedFilter_AppliesFilterAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
|
||||
var chatMessageStoreMessages = new List<ChatMessage> { new(ChatRole.System, "System") };
|
||||
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response") };
|
||||
var context = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
|
||||
ChatMessageStore.InvokedContext? capturedContext = null;
|
||||
innerStoreMock
|
||||
.Setup(s => s.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ChatMessageStore.InvokedContext, CancellationToken>((ctx, ct) => capturedContext = ctx)
|
||||
.Returns(default(ValueTask));
|
||||
|
||||
// Filter that modifies the context
|
||||
ChatMessageStore.InvokedContext InvokedFilter(ChatMessageStore.InvokedContext ctx)
|
||||
{
|
||||
var modifiedRequestMessages = ctx.RequestMessages.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
|
||||
return new ChatMessageStore.InvokedContext(modifiedRequestMessages, ctx.ChatMessageStoreMessages)
|
||||
{
|
||||
ResponseMessages = ctx.ResponseMessages,
|
||||
AIContextProviderMessages = ctx.AIContextProviderMessages,
|
||||
InvokeException = ctx.InvokeException
|
||||
};
|
||||
}
|
||||
|
||||
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, invokedMessagesFilter: InvokedFilter);
|
||||
|
||||
// Act
|
||||
await filter.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedContext);
|
||||
Assert.Single(capturedContext.RequestMessages);
|
||||
Assert.Equal("[FILTERED] Hello", capturedContext.RequestMessages.First().Text);
|
||||
innerStoreMock.Verify(s => s.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_DelegatesToInnerStore()
|
||||
{
|
||||
// Arrange
|
||||
var innerStoreMock = new Mock<ChatMessageStore>();
|
||||
var expectedJson = JsonSerializer.SerializeToElement("data", TestJsonSerializerContext.Default.String);
|
||||
|
||||
innerStoreMock
|
||||
.Setup(s => s.Serialize(It.IsAny<JsonSerializerOptions>()))
|
||||
.Returns(expectedJson);
|
||||
|
||||
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, x => x, x => x);
|
||||
|
||||
// Act
|
||||
var result = filter.Serialize();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedJson.GetRawText(), result.GetRawText());
|
||||
innerStoreMock.Verify(s => s.Serialize(null), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -78,11 +78,11 @@ public class ChatMessageStoreTests
|
||||
|
||||
private sealed class TestChatMessageStore : ChatMessageStore
|
||||
{
|
||||
public override Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IEnumerable<ChatMessage>>([]);
|
||||
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> new(Array.Empty<ChatMessage>());
|
||||
|
||||
public override Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
=> default;
|
||||
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> default;
|
||||
|
||||
@@ -38,19 +38,21 @@ public class DelegatingAIAgentTests
|
||||
this._innerAgentMock.Setup(x => x.GetNewThread()).Returns(this._testThread);
|
||||
|
||||
this._innerAgentMock
|
||||
.Setup(x => x.RunAsync(
|
||||
It.IsAny<IReadOnlyCollection<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(this._testResponse);
|
||||
|
||||
this._innerAgentMock
|
||||
.Setup(x => x.RunStreamingAsync(
|
||||
It.IsAny<IReadOnlyCollection<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Protected()
|
||||
.Setup<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerableAsync(this._testStreamingResponses));
|
||||
|
||||
this._delegatingAgent = new TestDelegatingAIAgent(this._innerAgentMock.Object);
|
||||
@@ -159,7 +161,12 @@ public class DelegatingAIAgentTests
|
||||
|
||||
var innerAgentMock = new Mock<AIAgent>();
|
||||
innerAgentMock
|
||||
.Setup(x => x.RunAsync(expectedMessages, expectedThread, expectedOptions, expectedCancellationToken))
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(m => m == expectedMessages),
|
||||
ItExpr.Is<AgentThread?>(t => t == expectedThread),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == expectedOptions),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == expectedCancellationToken))
|
||||
.Returns(expectedResult.Task);
|
||||
|
||||
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
|
||||
@@ -193,7 +200,12 @@ public class DelegatingAIAgentTests
|
||||
|
||||
var innerAgentMock = new Mock<AIAgent>();
|
||||
innerAgentMock
|
||||
.Setup(x => x.RunStreamingAsync(expectedMessages, expectedThread, expectedOptions, expectedCancellationToken))
|
||||
.Protected()
|
||||
.Setup<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(m => m == expectedMessages),
|
||||
ItExpr.Is<AgentThread?>(t => t == expectedThread),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == expectedOptions),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == expectedCancellationToken))
|
||||
.Returns(ToAsyncEnumerableAsync(expectedResults));
|
||||
|
||||
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
|
||||
|
||||
+51
-21
@@ -47,34 +47,54 @@ public class InMemoryChatMessageStoreTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsyncAddsMessagesAndReturnsNullThreadIdAsync()
|
||||
public async Task InvokedAsyncAddsMessagesAsync()
|
||||
{
|
||||
var store = new InMemoryChatMessageStore();
|
||||
var messages = new List<ChatMessage>
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello")
|
||||
};
|
||||
var responseMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var messageStoreMessages = new List<ChatMessage>()
|
||||
{
|
||||
new(ChatRole.System, "original instructions")
|
||||
};
|
||||
var aiContextProviderMessages = new List<ChatMessage>()
|
||||
{
|
||||
new(ChatRole.System, "additional context")
|
||||
};
|
||||
|
||||
await store.AddMessagesAsync(messages, CancellationToken.None);
|
||||
var store = new InMemoryChatMessageStore();
|
||||
store.Add(messageStoreMessages[0]);
|
||||
var context = new ChatMessageStore.InvokedContext(requestMessages, messageStoreMessages)
|
||||
{
|
||||
AIContextProviderMessages = aiContextProviderMessages,
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
await store.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, store.Count);
|
||||
Assert.Equal("Hello", store[0].Text);
|
||||
Assert.Equal("Hi there!", store[1].Text);
|
||||
Assert.Equal(4, store.Count);
|
||||
Assert.Equal("original instructions", store[0].Text);
|
||||
Assert.Equal("Hello", store[1].Text);
|
||||
Assert.Equal("additional context", store[2].Text);
|
||||
Assert.Equal("Hi there!", store[3].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsyncWithEmptyDoesNotFailAsync()
|
||||
public async Task InvokedAsyncWithEmptyDoesNotFailAsync()
|
||||
{
|
||||
var store = new InMemoryChatMessageStore();
|
||||
|
||||
await store.AddMessagesAsync([], CancellationToken.None);
|
||||
var context = new ChatMessageStore.InvokedContext([], []);
|
||||
await store.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Empty(store);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessagesAsyncReturnsAllMessagesAsync()
|
||||
public async Task InvokingAsyncReturnsAllMessagesAsync()
|
||||
{
|
||||
var store = new InMemoryChatMessageStore
|
||||
{
|
||||
@@ -82,7 +102,8 @@ public class InMemoryChatMessageStoreTests
|
||||
new ChatMessage(ChatRole.Assistant, "Test2")
|
||||
};
|
||||
|
||||
var result = (await store.GetMessagesAsync(CancellationToken.None)).ToList();
|
||||
var context = new ChatMessageStore.InvokingContext([]);
|
||||
var result = (await store.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, m => m.Text == "Test1");
|
||||
@@ -157,24 +178,25 @@ public class InMemoryChatMessageStoreTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsyncWithEmptyMessagesDoesNotChangeStoreAsync()
|
||||
public async Task InvokedAsyncWithEmptyMessagesDoesNotChangeStoreAsync()
|
||||
{
|
||||
var store = new InMemoryChatMessageStore();
|
||||
var messages = new List<ChatMessage>();
|
||||
|
||||
await store.AddMessagesAsync(messages, CancellationToken.None);
|
||||
var context = new ChatMessageStore.InvokedContext(messages, []);
|
||||
await store.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Empty(store);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsync_WithNullMessages_ThrowsArgumentNullExceptionAsync()
|
||||
public async Task InvokedAsync_WithNullContext_ThrowsArgumentNullExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
var store = new InMemoryChatMessageStore();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => store.AddMessagesAsync(null!, CancellationToken.None));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => store.InvokedAsync(null!, CancellationToken.None).AsTask());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -498,7 +520,8 @@ public class InMemoryChatMessageStoreTests
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded);
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync(originalMessages, CancellationToken.None);
|
||||
var context = new ChatMessageStore.InvokedContext(originalMessages, []);
|
||||
await store.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Single(store);
|
||||
@@ -526,10 +549,15 @@ public class InMemoryChatMessageStoreTests
|
||||
.ReturnsAsync(reducedMessages);
|
||||
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
|
||||
await store.AddMessagesAsync(originalMessages, CancellationToken.None);
|
||||
// Add messages directly to the store for this test
|
||||
foreach (var msg in originalMessages)
|
||||
{
|
||||
store.Add(msg);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = (await store.GetMessagesAsync(CancellationToken.None)).ToList();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext(Array.Empty<ChatMessage>());
|
||||
var result = (await store.InvokingAsync(invokingContext, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
@@ -551,7 +579,8 @@ public class InMemoryChatMessageStoreTests
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync(originalMessages, CancellationToken.None);
|
||||
var context = new ChatMessageStore.InvokedContext(originalMessages, []);
|
||||
await store.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Single(store);
|
||||
@@ -576,7 +605,8 @@ public class InMemoryChatMessageStoreTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = (await store.GetMessagesAsync(CancellationToken.None)).ToList();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext(Array.Empty<ChatMessage>());
|
||||
var result = (await store.InvokingAsync(invokingContext, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
|
||||
+100
-42
@@ -202,11 +202,11 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddMessagesAsync Tests
|
||||
#region InvokedAsync Tests
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task AddMessagesAsync_WithSingleMessage_ShouldAddMessageAsync()
|
||||
public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
@@ -214,14 +214,20 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
|
||||
var message = new ChatMessage(ChatRole.User, "Hello, world!");
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext([message], [])
|
||||
{
|
||||
ResponseMessages = []
|
||||
};
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync([message]);
|
||||
await store.InvokedAsync(context);
|
||||
|
||||
// Wait a moment for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
var messages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var messages = await store.InvokingAsync(invokingContext);
|
||||
var messageList = messages.ToList();
|
||||
|
||||
// Simple assertion - if this fails, we know the deserialization is the issue
|
||||
@@ -256,7 +262,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
}
|
||||
|
||||
string rawJson = rawResults.Count > 0 ? Newtonsoft.Json.JsonConvert.SerializeObject(rawResults[0], Newtonsoft.Json.Formatting.Indented) : "null";
|
||||
Assert.Fail($"GetMessagesAsync returned 0 messages, but direct count query found {count} items for conversation {conversationId}. Raw document: {rawJson}");
|
||||
Assert.Fail($"InvokingAsync returned 0 messages, but direct count query found {count} items for conversation {conversationId}. Raw document: {rawJson}");
|
||||
}
|
||||
|
||||
Assert.Single(messageList);
|
||||
@@ -266,45 +272,63 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task AddMessagesAsync_WithMultipleMessages_ShouldAddAllMessagesAsync()
|
||||
public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
var conversationId = Guid.NewGuid().ToString();
|
||||
using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
|
||||
var messages = new[]
|
||||
var requestMessages = new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.User, "First message"),
|
||||
new ChatMessage(ChatRole.Assistant, "Second message"),
|
||||
new ChatMessage(ChatRole.User, "Third message")
|
||||
};
|
||||
var aiContextProviderMessages = new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.System, "System context message")
|
||||
};
|
||||
var responseMessages = new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.Assistant, "Response message")
|
||||
};
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext(requestMessages, [])
|
||||
{
|
||||
AIContextProviderMessages = aiContextProviderMessages,
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync(messages);
|
||||
await store.InvokedAsync(context);
|
||||
|
||||
// Assert
|
||||
var retrievedMessages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var retrievedMessages = await store.InvokingAsync(invokingContext);
|
||||
var messageList = retrievedMessages.ToList();
|
||||
Assert.Equal(3, messageList.Count);
|
||||
Assert.Equal(5, messageList.Count);
|
||||
Assert.Equal("First message", messageList[0].Text);
|
||||
Assert.Equal("Second message", messageList[1].Text);
|
||||
Assert.Equal("Third message", messageList[2].Text);
|
||||
Assert.Equal("System context message", messageList[3].Text);
|
||||
Assert.Equal("Response message", messageList[4].Text);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetMessagesAsync Tests
|
||||
#region InvokingAsync Tests
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task GetMessagesAsync_WithNoMessages_ShouldReturnEmptyAsync()
|
||||
public async Task InvokingAsync_WithNoMessages_ShouldReturnEmptyAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString());
|
||||
|
||||
// Act
|
||||
var messages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var messages = await store.InvokingAsync(invokingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(messages);
|
||||
@@ -312,7 +336,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task GetMessagesAsync_WithConversationIsolation_ShouldOnlyReturnMessagesForConversationAsync()
|
||||
public async Task InvokingAsync_WithConversationIsolation_ShouldOnlyReturnMessagesForConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
@@ -322,12 +346,18 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
using var store1 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversation1);
|
||||
using var store2 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversation2);
|
||||
|
||||
await store1.AddMessagesAsync([new ChatMessage(ChatRole.User, "Message for conversation 1")]);
|
||||
await store2.AddMessagesAsync([new ChatMessage(ChatRole.User, "Message for conversation 2")]);
|
||||
var context1 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 1")], []);
|
||||
var context2 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 2")], []);
|
||||
|
||||
await store1.InvokedAsync(context1);
|
||||
await store2.InvokedAsync(context2);
|
||||
|
||||
// Act
|
||||
var messages1 = await store1.GetMessagesAsync();
|
||||
var messages2 = await store2.GetMessagesAsync();
|
||||
var invokingContext1 = new ChatMessageStore.InvokingContext([]);
|
||||
var invokingContext2 = new ChatMessageStore.InvokingContext([]);
|
||||
|
||||
var messages1 = await store1.InvokingAsync(invokingContext1);
|
||||
var messages2 = await store2.InvokingAsync(invokingContext2);
|
||||
|
||||
// Assert
|
||||
var messageList1 = messages1.ToList();
|
||||
@@ -361,16 +391,18 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
};
|
||||
|
||||
// Act 1: Add messages
|
||||
await originalStore.AddMessagesAsync(messages);
|
||||
var invokedContext = new ChatMessageStore.InvokedContext(messages, []);
|
||||
await originalStore.InvokedAsync(invokedContext);
|
||||
|
||||
// Act 2: Verify messages were added
|
||||
var retrievedMessages = await originalStore.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var retrievedMessages = await originalStore.InvokingAsync(invokingContext);
|
||||
var retrievedList = retrievedMessages.ToList();
|
||||
Assert.Equal(5, retrievedList.Count);
|
||||
|
||||
// Act 3: Create new store instance for same conversation (test persistence)
|
||||
using var newStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
|
||||
var persistedMessages = await newStore.GetMessagesAsync();
|
||||
var persistedMessages = await newStore.InvokingAsync(invokingContext);
|
||||
var persistedList = persistedMessages.ToList();
|
||||
|
||||
// Assert final state
|
||||
@@ -502,7 +534,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task AddMessagesAsync_WithHierarchicalPartitioning_ShouldAddMessageWithMetadataAsync()
|
||||
public async Task InvokedAsync_WithHierarchicalPartitioning_ShouldAddMessageWithMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
@@ -513,14 +545,17 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
using var store = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
|
||||
var message = new ChatMessage(ChatRole.User, "Hello from hierarchical partitioning!");
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext([message], []);
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync([message]);
|
||||
await store.InvokedAsync(context);
|
||||
|
||||
// Wait a moment for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
var messages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var messages = await store.InvokingAsync(invokingContext);
|
||||
var messageList = messages.ToList();
|
||||
|
||||
Assert.Single(messageList);
|
||||
@@ -551,7 +586,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task AddMessagesAsync_WithHierarchicalMultipleMessages_ShouldAddAllMessagesAsync()
|
||||
public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
@@ -567,14 +602,17 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
new ChatMessage(ChatRole.User, "Third hierarchical message")
|
||||
};
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext(messages, []);
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync(messages);
|
||||
await store.InvokedAsync(context);
|
||||
|
||||
// Wait a moment for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
var retrievedMessages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var retrievedMessages = await store.InvokingAsync(invokingContext);
|
||||
var messageList = retrievedMessages.ToList();
|
||||
|
||||
Assert.Equal(3, messageList.Count);
|
||||
@@ -585,7 +623,7 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task GetMessagesAsync_WithHierarchicalPartitionIsolation_ShouldIsolateMessagesByUserIdAsync()
|
||||
public async Task InvokingAsync_WithHierarchicalPartitionIsolation_ShouldIsolateMessagesByUserIdAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
@@ -599,17 +637,23 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
using var store2 = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId2, SessionId);
|
||||
|
||||
// Add messages to both stores
|
||||
await store1.AddMessagesAsync([new ChatMessage(ChatRole.User, "Message from user 1")]);
|
||||
await store2.AddMessagesAsync([new ChatMessage(ChatRole.User, "Message from user 2")]);
|
||||
var context1 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 1")], []);
|
||||
var context2 = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 2")], []);
|
||||
|
||||
await store1.InvokedAsync(context1);
|
||||
await store2.InvokedAsync(context2);
|
||||
|
||||
// Wait a moment for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act & Assert
|
||||
var messages1 = await store1.GetMessagesAsync();
|
||||
var invokingContext1 = new ChatMessageStore.InvokingContext([]);
|
||||
var invokingContext2 = new ChatMessageStore.InvokingContext([]);
|
||||
|
||||
var messages1 = await store1.InvokingAsync(invokingContext1);
|
||||
var messageList1 = messages1.ToList();
|
||||
|
||||
var messages2 = await store2.GetMessagesAsync();
|
||||
var messages2 = await store2.InvokingAsync(invokingContext2);
|
||||
var messageList2 = messages2.ToList();
|
||||
|
||||
// With true hierarchical partitioning, each user sees only their own messages
|
||||
@@ -630,7 +674,9 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
const string SessionId = "session-serialize";
|
||||
|
||||
using var originalStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
|
||||
await originalStore.AddMessagesAsync([new ChatMessage(ChatRole.User, "Test serialization message")]);
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Test serialization message")], []);
|
||||
await originalStore.InvokedAsync(context);
|
||||
|
||||
// Act - Serialize the store state
|
||||
var serializedState = originalStore.Serialize();
|
||||
@@ -647,7 +693,8 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - The deserialized store should have the same functionality
|
||||
var messages = await deserializedStore.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var messages = await deserializedStore.InvokingAsync(invokingContext);
|
||||
var messageList = messages.ToList();
|
||||
|
||||
Assert.Single(messageList);
|
||||
@@ -670,17 +717,22 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
using var hierarchicalStore = new CosmosChatMessageStore(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-coexist", "user-coexist", SessionId);
|
||||
|
||||
// Add messages to both
|
||||
await simpleStore.AddMessagesAsync([new ChatMessage(ChatRole.User, "Simple partitioning message")]);
|
||||
await hierarchicalStore.AddMessagesAsync([new ChatMessage(ChatRole.User, "Hierarchical partitioning message")]);
|
||||
var simpleContext = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Simple partitioning message")], []);
|
||||
var hierarchicalContext = new ChatMessageStore.InvokedContext([new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []);
|
||||
|
||||
await simpleStore.InvokedAsync(simpleContext);
|
||||
await hierarchicalStore.InvokedAsync(hierarchicalContext);
|
||||
|
||||
// Wait a moment for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act & Assert
|
||||
var simpleMessages = await simpleStore.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
|
||||
var simpleMessages = await simpleStore.InvokingAsync(invokingContext);
|
||||
var simpleMessageList = simpleMessages.ToList();
|
||||
|
||||
var hierarchicalMessages = await hierarchicalStore.GetMessagesAsync();
|
||||
var hierarchicalMessages = await hierarchicalStore.InvokingAsync(invokingContext);
|
||||
var hierarchicalMessageList = hierarchicalMessages.ToList();
|
||||
|
||||
// Each should only see its own messages since they use different containers
|
||||
@@ -707,14 +759,17 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
messages.Add(new ChatMessage(ChatRole.User, $"Message {i}"));
|
||||
await Task.Delay(10); // Small delay to ensure different timestamps
|
||||
}
|
||||
await store.AddMessagesAsync(messages);
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext(messages, []);
|
||||
await store.InvokedAsync(context);
|
||||
|
||||
// Wait for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act - Set max to 5 and retrieve
|
||||
store.MaxMessagesToRetrieve = 5;
|
||||
var retrievedMessages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var retrievedMessages = await store.InvokingAsync(invokingContext);
|
||||
var messageList = retrievedMessages.ToList();
|
||||
|
||||
// Assert - Should get the 5 most recent messages (6-10) in ascending order
|
||||
@@ -742,13 +797,16 @@ public sealed class CosmosChatMessageStoreTests : IAsyncLifetime, IDisposable
|
||||
{
|
||||
messages.Add(new ChatMessage(ChatRole.User, $"Message {i}"));
|
||||
}
|
||||
await store.AddMessagesAsync(messages);
|
||||
|
||||
var context = new ChatMessageStore.InvokedContext(messages, []);
|
||||
await store.InvokedAsync(context);
|
||||
|
||||
// Wait for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act - No limit set (default null)
|
||||
var retrievedMessages = await store.GetMessagesAsync();
|
||||
var invokingContext = new ChatMessageStore.InvokingContext([]);
|
||||
var retrievedMessages = await store.InvokingAsync(invokingContext);
|
||||
var messageList = retrievedMessages.ToList();
|
||||
|
||||
// Assert - Should get all 10 messages
|
||||
|
||||
+2
-2
@@ -76,12 +76,12 @@ public sealed class AggregatorPromptAgentFactoryTests
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
+4
-4
@@ -290,7 +290,7 @@ internal sealed class FakeChatClientAgent : AIAgent
|
||||
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -305,7 +305,7 @@ internal sealed class FakeChatClientAgent : AIAgent
|
||||
return updates.ToAgentRunResponse();
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -358,7 +358,7 @@ internal sealed class FakeMultiMessageAgent : AIAgent
|
||||
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -373,7 +373,7 @@ internal sealed class FakeMultiMessageAgent : AIAgent
|
||||
return updates.ToAgentRunResponse();
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -303,12 +303,12 @@ internal sealed class FakeForwardedPropsAgent : AIAgent
|
||||
|
||||
public JsonElement ReceivedForwardedProperties { get; private set; }
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -342,12 +342,12 @@ internal sealed class FakeStateAgent : AIAgent
|
||||
{
|
||||
public override string? Description => "Agent for state testing";
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+4
-4
@@ -430,12 +430,12 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
|
||||
new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -519,12 +519,12 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
|
||||
new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -17,13 +17,13 @@ internal sealed class TestAgent(string name, string description) : AIAgent
|
||||
JsonElement serializedThread,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null) => new DummyAgentThread();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(new AgentRunResponse([.. messages]));
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+25
-12
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
|
||||
using ChatRole = Microsoft.Extensions.AI.ChatRole;
|
||||
using OpenAIChatMessage = OpenAI.Chat.ChatMessage;
|
||||
@@ -76,22 +77,28 @@ public sealed class AIAgentWithOpenAIExtensionsTests
|
||||
var responseMessage = new ChatMessage(ChatRole.Assistant, [new TextContent(ResponseText)]);
|
||||
|
||||
mockAgent
|
||||
.Setup(a => a.RunAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<AgentThread?>(), It.IsAny<AgentRunOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new AgentRunResponse([responseMessage]));
|
||||
|
||||
// Act
|
||||
var result = await mockAgent.Object.RunAsync(openAiMessages, mockThread.Object, options, cancellationToken);
|
||||
|
||||
// Assert
|
||||
mockAgent.Verify(
|
||||
a => a.RunAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs =>
|
||||
mockAgent.Protected()
|
||||
.Verify("RunCoreAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(msgs =>
|
||||
msgs.ToList().Count == 1 &&
|
||||
msgs.ToList()[0].Text == TestMessageText),
|
||||
mockThread.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result.Content);
|
||||
@@ -160,7 +167,12 @@ public sealed class AIAgentWithOpenAIExtensionsTests
|
||||
};
|
||||
|
||||
mockAgent
|
||||
.Setup(a => a.RunStreamingAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<AgentThread?>(), It.IsAny<AgentRunOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.Protected()
|
||||
.Setup<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerableAsync(responseUpdates));
|
||||
|
||||
// Act
|
||||
@@ -172,15 +184,16 @@ public sealed class AIAgentWithOpenAIExtensionsTests
|
||||
}
|
||||
|
||||
// Assert
|
||||
mockAgent.Verify(
|
||||
a => a.RunStreamingAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs =>
|
||||
mockAgent.Protected()
|
||||
.Verify("RunCoreStreamingAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(msgs =>
|
||||
msgs.ToList().Count == 1 &&
|
||||
msgs.ToList()[0].Text == TestMessageText),
|
||||
mockThread.Object,
|
||||
options,
|
||||
cancellationToken),
|
||||
Times.Once);
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
Assert.True(updateCount > 0, "Expected at least one streaming update");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Agents.AI.Purview.Models.Common;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Purview.UnitTests;
|
||||
|
||||
@@ -277,11 +278,13 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
Assert.Single(result.Messages);
|
||||
Assert.Equal(ChatRole.System, result.Messages[0].Role);
|
||||
Assert.Equal("Prompt blocked by policy", result.Messages[0].Text);
|
||||
mockAgent.Verify(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
|
||||
mockAgent.Protected().Verify("RunCoreAsync",
|
||||
Times.Never(),
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -295,11 +298,12 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
var innerResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Sensitive response"));
|
||||
|
||||
mockAgent.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
mockAgent.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(innerResponse);
|
||||
|
||||
this._mockProcessor.SetupSequence(x => x.ProcessMessagesAsync(
|
||||
@@ -333,11 +337,12 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
var innerResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Safe response"));
|
||||
|
||||
mockAgent.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
mockAgent.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(innerResponse);
|
||||
|
||||
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
|
||||
@@ -375,11 +380,12 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
|
||||
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response from inner agent"));
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
mockAgent.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
this._mockProcessor.SetupSequence(x => x.ProcessMessagesAsync(
|
||||
@@ -441,11 +447,12 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
|
||||
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response"));
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
mockAgent.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
|
||||
@@ -482,11 +489,12 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
|
||||
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response"));
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
mockAgent.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
string? capturedThreadId = null;
|
||||
@@ -521,11 +529,12 @@ public sealed class PurviewWrapperTests : IDisposable
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
var innerResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response"));
|
||||
|
||||
mockAgent.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread>(),
|
||||
It.IsAny<AgentRunOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
mockAgent.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread>(),
|
||||
ItExpr.IsAny<AgentRunOptions>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(innerResponse);
|
||||
|
||||
var callCount = 0;
|
||||
|
||||
@@ -337,7 +337,7 @@ public class AgentExtensionsTests
|
||||
public CancellationToken LastCancellationToken { get; private set; }
|
||||
public int RunAsyncCallCount { get; private set; }
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -355,7 +355,7 @@ public class AgentExtensionsTests
|
||||
return Task.FromResult(this._responseToReturn!);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests;
|
||||
|
||||
@@ -35,18 +36,22 @@ public class AnonymousDelegatingAIAgentTests
|
||||
new AgentRunResponseUpdate(ChatRole.Assistant, "Response 2")
|
||||
];
|
||||
|
||||
this._innerAgentMock.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
this._innerAgentMock
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(this._testResponse);
|
||||
|
||||
this._innerAgentMock.Setup(x => x.RunStreamingAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
this._innerAgentMock
|
||||
.Protected()
|
||||
.Setup<IAsyncEnumerable<AgentRunResponseUpdate>>("RunCoreStreamingAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerableAsync(this._testStreamingResponses));
|
||||
}
|
||||
|
||||
@@ -184,11 +189,14 @@ public class AnonymousDelegatingAIAgentTests
|
||||
Assert.Same(this._testOptions, capturedOptions);
|
||||
Assert.Equal(expectedCancellationToken, capturedCancellationToken);
|
||||
|
||||
this._innerAgentMock.Verify(x => x.RunAsync(
|
||||
this._testMessages,
|
||||
this._testThread,
|
||||
this._testOptions,
|
||||
expectedCancellationToken), Times.Once);
|
||||
this._innerAgentMock
|
||||
.Protected()
|
||||
.Verify<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
Times.Once(),
|
||||
ItExpr.Is<IEnumerable<ChatMessage>>(m => m == this._testMessages),
|
||||
ItExpr.Is<AgentThread?>(t => t == this._testThread),
|
||||
ItExpr.Is<AgentRunOptions?>(o => o == this._testOptions),
|
||||
ItExpr.Is<CancellationToken>(ct => ct == expectedCancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -458,11 +466,13 @@ public class AnonymousDelegatingAIAgentTests
|
||||
capturedValue = asyncLocal.Value;
|
||||
});
|
||||
|
||||
this._innerAgentMock.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
this._innerAgentMock
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.Returns(() =>
|
||||
{
|
||||
// Verify AsyncLocal value is available in inner agent call
|
||||
@@ -926,11 +936,13 @@ public class AnonymousDelegatingAIAgentTests
|
||||
var capturedTokens = new List<CancellationToken>();
|
||||
|
||||
// Setup mock to throw OperationCanceledException when cancelled token is used
|
||||
this._innerAgentMock.Setup(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.Is<CancellationToken>(ct => ct.IsCancellationRequested)))
|
||||
this._innerAgentMock
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.Is<CancellationToken>(ct => ct.IsCancellationRequested))
|
||||
.ThrowsAsync(new OperationCanceledException());
|
||||
|
||||
var agent = new AIAgentBuilder(this._innerAgentMock.Object)
|
||||
@@ -993,11 +1005,14 @@ public class AnonymousDelegatingAIAgentTests
|
||||
Assert.Equal(expectedOrder, executionOrder);
|
||||
|
||||
// Verify inner agent was never called
|
||||
this._innerAgentMock.Verify(x => x.RunAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<AgentThread?>(),
|
||||
It.IsAny<AgentRunOptions?>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
this._innerAgentMock
|
||||
.Protected()
|
||||
.Verify<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
Times.Never(),
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -502,6 +502,12 @@ public partial class ChatClientAgentTests
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
Mock<ChatMessageStore> mockChatMessageStore = new();
|
||||
mockChatMessageStore.Setup(s => s.InvokingAsync(
|
||||
It.IsAny<ChatMessageStore.InvokingContext>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
|
||||
mockChatMessageStore.Setup(s => s.InvokedAsync(
|
||||
It.IsAny<ChatMessageStore.InvokedContext>(),
|
||||
It.IsAny<CancellationToken>())).Returns(new ValueTask());
|
||||
|
||||
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
|
||||
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(mockChatMessageStore.Object);
|
||||
@@ -518,7 +524,58 @@ public partial class ChatClientAgentTests
|
||||
|
||||
// Assert
|
||||
Assert.IsType<ChatMessageStore>(thread!.MessageStore, exactMatch: false);
|
||||
mockChatMessageStore.Verify(s => s.AddMessagesAsync(It.Is<IEnumerable<ChatMessage>>(x => x.Count() == 2), It.IsAny<CancellationToken>()), Times.Once);
|
||||
mockService.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Count() == 2 && msgs.Any(m => m.Text == "Existing Chat History") && msgs.Any(m => m.Text == "test")),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
mockChatMessageStore.Verify(s => s.InvokingAsync(
|
||||
It.Is<ChatMessageStore.InvokingContext>(x => x.RequestMessages.Count() == 1),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
mockChatMessageStore.Verify(s => s.InvokedAsync(
|
||||
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ChatMessageStoreMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync notifies the ChatMessageStore on failure.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncNotifiesChatMessageStoreOnFailureAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).Throws(new InvalidOperationException("Test Error"));
|
||||
|
||||
Mock<ChatMessageStore> mockChatMessageStore = new();
|
||||
|
||||
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
|
||||
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(mockChatMessageStore.Object);
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "test instructions" },
|
||||
ChatMessageStoreFactory = mockFactory.Object
|
||||
});
|
||||
|
||||
// Act
|
||||
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
|
||||
|
||||
// Assert
|
||||
Assert.IsType<ChatMessageStore>(thread!.MessageStore, exactMatch: false);
|
||||
mockChatMessageStore.Verify(s => s.InvokedAsync(
|
||||
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>()), Times.Once);
|
||||
}
|
||||
|
||||
@@ -599,18 +656,18 @@ public partial class ChatClientAgentTests
|
||||
await agent.RunAsync(requestMessages, thread);
|
||||
|
||||
// Assert
|
||||
// Should contain: base instructions, context message, user message, base function, context function
|
||||
// Should contain: base instructions, user message, context message, base function, context function
|
||||
Assert.Equal(2, capturedMessages.Count);
|
||||
Assert.Equal("base instructions\ncontext provider instructions", capturedInstructions);
|
||||
Assert.Equal("context provider message", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.System, capturedMessages[0].Role);
|
||||
Assert.Equal("user message", capturedMessages[1].Text);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[1].Role);
|
||||
Assert.Equal("user message", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[0].Role);
|
||||
Assert.Equal("context provider message", capturedMessages[1].Text);
|
||||
Assert.Equal(ChatRole.System, capturedMessages[1].Role);
|
||||
Assert.Equal(2, capturedTools.Count);
|
||||
Assert.Contains(capturedTools, t => t.Name == "base function");
|
||||
Assert.Contains(capturedTools, t => t.Name == "context provider function");
|
||||
|
||||
// Verify that the thread was updated with the input, ai context and response messages
|
||||
// Verify that the thread was updated with the ai context provider, input and response messages
|
||||
var messageStore = Assert.IsType<InMemoryChatMessageStore>(thread!.MessageStore);
|
||||
Assert.Equal(3, messageStore.Count);
|
||||
Assert.Equal("user message", messageStore[0].Text);
|
||||
@@ -2056,18 +2113,18 @@ public partial class ChatClientAgentTests
|
||||
_ = await updates.ToAgentRunResponseAsync();
|
||||
|
||||
// Assert
|
||||
// Should contain: base instructions, context message, user message, base function, context function
|
||||
// Should contain: base instructions, user message, context message, base function, context function
|
||||
Assert.Equal(2, capturedMessages.Count);
|
||||
Assert.Equal("base instructions\ncontext provider instructions", capturedInstructions);
|
||||
Assert.Equal("context provider message", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.System, capturedMessages[0].Role);
|
||||
Assert.Equal("user message", capturedMessages[1].Text);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[1].Role);
|
||||
Assert.Equal("user message", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[0].Role);
|
||||
Assert.Equal("context provider message", capturedMessages[1].Text);
|
||||
Assert.Equal(ChatRole.System, capturedMessages[1].Role);
|
||||
Assert.Equal(2, capturedTools.Count);
|
||||
Assert.Contains(capturedTools, t => t.Name == "base function");
|
||||
Assert.Contains(capturedTools, t => t.Name == "context provider function");
|
||||
|
||||
// Verify that the thread was updated with the input, ai context and response messages
|
||||
// Verify that the thread was updated with the input, ai context provider, and response messages
|
||||
var messageStore = Assert.IsType<InMemoryChatMessageStore>(thread!.MessageStore);
|
||||
Assert.Equal(3, messageStore.Count);
|
||||
Assert.Equal("user message", messageStore[0].Text);
|
||||
|
||||
+4
-4
@@ -339,7 +339,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
// Create a mock message store that would normally provide messages
|
||||
var mockMessageStore = new Mock<ChatMessageStore>();
|
||||
mockMessageStore
|
||||
.Setup(ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()))
|
||||
.Setup(ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
|
||||
|
||||
// Create a mock AI context provider that would normally provide context
|
||||
@@ -383,7 +383,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
|
||||
// Verify that message store was never called due to continuation token
|
||||
mockMessageStore.Verify(
|
||||
ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()),
|
||||
ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
|
||||
// Verify that AI context provider was never called due to continuation token
|
||||
@@ -401,7 +401,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
// Create a mock message store that would normally provide messages
|
||||
var mockMessageStore = new Mock<ChatMessageStore>();
|
||||
mockMessageStore
|
||||
.Setup(ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()))
|
||||
.Setup(ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
|
||||
|
||||
// Create a mock AI context provider that would normally provide context
|
||||
@@ -446,7 +446,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
|
||||
// Verify that message store was never called due to continuation token
|
||||
mockMessageStore.Verify(
|
||||
ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()),
|
||||
ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
|
||||
// Verify that AI context provider was never called due to continuation token
|
||||
|
||||
@@ -30,10 +30,10 @@ internal sealed class TestAIAgent : AIAgent
|
||||
public override AgentThread GetNewThread() =>
|
||||
this.GetNewThreadFunc();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
this.RunAsyncFunc(messages, thread, options, cancellationToken);
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsyncFunc(messages, thread, options, cancellationToken);
|
||||
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
|
||||
@@ -141,11 +141,11 @@ public class AgentWorkflowBuilderTests
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> new DoubleEchoAgentThread();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.Yield();
|
||||
@@ -409,7 +409,7 @@ public class AgentWorkflowBuilderTests
|
||||
|
||||
private sealed class DoubleEchoAgentWithBarrier(string name, StrongBox<TaskCompletionSource<bool>> barrier, StrongBox<int> remaining) : DoubleEchoAgent(name)
|
||||
{
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Interlocked.Decrement(ref remaining.Value) == 0)
|
||||
@@ -419,7 +419,7 @@ public class AgentWorkflowBuilderTests
|
||||
|
||||
await barrier.Value!.Task.ConfigureAwait(false);
|
||||
|
||||
await foreach (var update in base.RunStreamingAsync(messages, thread, options, cancellationToken))
|
||||
await foreach (var update in base.RunCoreStreamingAsync(messages, thread, options, cancellationToken))
|
||||
{
|
||||
await Task.Yield();
|
||||
yield return update;
|
||||
|
||||
@@ -149,7 +149,7 @@ public class InProcessExecutionTests
|
||||
public override AgentThread DeserializeThread(System.Text.Json.JsonElement serializedThread,
|
||||
System.Text.Json.JsonSerializerOptions? jsonSerializerOptions = null) => new SimpleTestAgentThread();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -160,7 +160,7 @@ public class InProcessExecutionTests
|
||||
return Task.FromResult(new AgentRunResponse(responseMessage));
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -30,10 +30,10 @@ public class RepresentationTests
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -66,17 +66,17 @@ internal sealed class HelloAgent(string id = nameof(HelloAgent)) : AIAgent
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> new HelloAgentThread();
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IEnumerable<AgentRunResponseUpdate> update = [
|
||||
await this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
await this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.SingleAsync(cancellationToken)
|
||||
.ConfigureAwait(false)];
|
||||
|
||||
return update.ToAgentRunResponse();
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
yield return new(ChatRole.Assistant, "Hello World!")
|
||||
{
|
||||
|
||||
+2
-2
@@ -62,14 +62,14 @@ public class SpecializedExecutorSmokeTests
|
||||
|
||||
public List<ChatMessage> Messages { get; } = Validate(messages) ?? [];
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new AgentRunResponse(this.Messages)
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = Guid.NewGuid().ToString("N")
|
||||
});
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string responseId = Guid.NewGuid().ToString("N");
|
||||
foreach (ChatMessage message in this.Messages)
|
||||
|
||||
@@ -60,7 +60,7 @@ internal class TestEchoAgent(string? id = null, string? name = null, string? pre
|
||||
return [];
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentRunResponse result =
|
||||
new(this.EchoMessages(messages, thread, options).ToList())
|
||||
@@ -73,7 +73,7 @@ internal class TestEchoAgent(string? id = null, string? name = null, string? pre
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string responseId = Guid.NewGuid().ToString("N");
|
||||
|
||||
|
||||
@@ -32,7 +32,12 @@ public class OpenAIChatCompletionFixture : IChatClientAgentFixture
|
||||
{
|
||||
var typedThread = (ChatClientAgentThread)thread;
|
||||
|
||||
return typedThread.MessageStore is null ? [] : (await typedThread.MessageStore.GetMessagesAsync()).ToList();
|
||||
if (typedThread.MessageStore is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await typedThread.MessageStore.InvokingAsync(new([]))).ToList();
|
||||
}
|
||||
|
||||
public Task<ChatClientAgent> CreateChatClientAgentAsync(
|
||||
|
||||
@@ -50,7 +50,12 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
|
||||
return [.. previousMessages, responseMessage];
|
||||
}
|
||||
|
||||
return typedThread.MessageStore is null ? [] : (await typedThread.MessageStore.GetMessagesAsync()).ToList();
|
||||
if (typedThread.MessageStore is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await typedThread.MessageStore.InvokingAsync(new([]))).ToList();
|
||||
}
|
||||
|
||||
private static ChatMessage ConvertToChatMessage(ResponseItem item)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import sys
|
||||
from collections.abc import Mapping, MutableSequence
|
||||
from typing import Any, ClassVar, TypeVar
|
||||
from typing import Any, ClassVar, TypeVar, cast
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
@@ -379,6 +379,15 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
"""Take ChatOptions and create the specific options for Azure AI."""
|
||||
prepared_messages, instructions = self._prepare_messages_for_azure_ai(messages)
|
||||
run_options = await super()._prepare_options(prepared_messages, chat_options, **kwargs)
|
||||
|
||||
# WORKAROUND: Azure AI Projects 'create responses' API has schema divergence from OpenAI's
|
||||
# Responses API. Azure requires 'type' at item level and 'annotations' in content items.
|
||||
# See: https://github.com/Azure/azure-sdk-for-python/issues/44493
|
||||
# See: https://github.com/microsoft/agent-framework/issues/2926
|
||||
# TODO(agent-framework#2926): Remove this workaround when Azure SDK aligns with OpenAI schema.
|
||||
if "input" in run_options and isinstance(run_options["input"], list):
|
||||
run_options["input"] = self._transform_input_for_azure_ai(cast(list[dict[str, Any]], run_options["input"]))
|
||||
|
||||
if not self._is_application_endpoint:
|
||||
# Application-scoped response APIs do not support "agent" property.
|
||||
agent_reference = await self._get_agent_reference_or_create(run_options, instructions)
|
||||
@@ -393,6 +402,44 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
|
||||
return run_options
|
||||
|
||||
def _transform_input_for_azure_ai(self, input_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Transform input items to match Azure AI Projects expected schema.
|
||||
|
||||
WORKAROUND: Azure AI Projects 'create responses' API expects a different schema than OpenAI's
|
||||
Responses API. Azure requires 'type' at the item level, and requires 'annotations'
|
||||
only for output_text content items (assistant messages), not for input_text content items
|
||||
(user messages). This helper adapts the OpenAI-style input to the Azure schema.
|
||||
|
||||
See: https://github.com/Azure/azure-sdk-for-python/issues/44493
|
||||
TODO(agent-framework#2926): Remove when Azure SDK aligns with OpenAI schema.
|
||||
"""
|
||||
transformed: list[dict[str, Any]] = []
|
||||
for item in input_items:
|
||||
new_item: dict[str, Any] = dict(item)
|
||||
|
||||
# Add 'type': 'message' at item level for role-based items
|
||||
if "role" in new_item and "type" not in new_item:
|
||||
new_item["type"] = "message"
|
||||
|
||||
# Add 'annotations' only to output_text content items (assistant messages)
|
||||
# User messages (input_text) do NOT support annotations in Azure AI
|
||||
if "content" in new_item and isinstance(new_item["content"], list):
|
||||
new_content: list[dict[str, Any] | Any] = []
|
||||
for content_item in new_item["content"]:
|
||||
if isinstance(content_item, dict):
|
||||
new_content_item: dict[str, Any] = dict(content_item)
|
||||
# Only add annotations to output_text (assistant content)
|
||||
if new_content_item.get("type") == "output_text" and "annotations" not in new_content_item:
|
||||
new_content_item["annotations"] = []
|
||||
new_content.append(new_content_item)
|
||||
else:
|
||||
new_content.append(content_item)
|
||||
new_item["content"] = new_content
|
||||
|
||||
transformed.append(new_item)
|
||||
|
||||
return transformed
|
||||
|
||||
@override
|
||||
def _get_current_conversation_id(self, chat_options: ChatOptions, **kwargs: Any) -> str | None:
|
||||
"""Get the current conversation ID from chat options or kwargs."""
|
||||
|
||||
@@ -286,6 +286,93 @@ async def test_azure_ai_client_prepare_messages_for_azure_ai_no_system_messages(
|
||||
assert instructions is None
|
||||
|
||||
|
||||
def test_azure_ai_client_transform_input_for_azure_ai(mock_project_client: MagicMock) -> None:
|
||||
"""Test _transform_input_for_azure_ai adds required fields for Azure AI schema.
|
||||
|
||||
WORKAROUND TEST: Azure AI Projects API requires 'type' at item level and
|
||||
'annotations' in output_text content items, which OpenAI's Responses API does not require.
|
||||
See: https://github.com/Azure/azure-sdk-for-python/issues/44493
|
||||
See: https://github.com/microsoft/agent-framework/issues/2926
|
||||
"""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
# Input in OpenAI Responses API format (what agent-framework generates)
|
||||
openai_format_input = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Hello"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "output_text", "text": "Hi there!"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = client._transform_input_for_azure_ai(openai_format_input) # type: ignore
|
||||
|
||||
# Verify 'type': 'message' added at item level
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[1]["type"] == "message"
|
||||
|
||||
# Verify 'annotations' added ONLY to output_text (assistant) content, NOT input_text (user)
|
||||
assert result[0]["content"][0]["type"] == "input_text" # user content type preserved
|
||||
assert "annotations" not in result[0]["content"][0] # user message - no annotations
|
||||
assert result[1]["content"][0]["type"] == "output_text" # assistant content type preserved
|
||||
assert result[1]["content"][0]["annotations"] == [] # assistant message - has annotations
|
||||
|
||||
# Verify original fields preserved
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"][0]["text"] == "Hello"
|
||||
assert result[1]["role"] == "assistant"
|
||||
assert result[1]["content"][0]["text"] == "Hi there!"
|
||||
|
||||
|
||||
def test_azure_ai_client_transform_input_preserves_existing_fields(mock_project_client: MagicMock) -> None:
|
||||
"""Test _transform_input_for_azure_ai preserves existing type and annotations."""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
# Input that already has the fields (shouldn't duplicate)
|
||||
input_with_fields = [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "output_text", "text": "Hello", "annotations": [{"some": "annotation"}]},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = client._transform_input_for_azure_ai(input_with_fields) # type: ignore
|
||||
|
||||
# Should preserve existing values, not overwrite
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[0]["content"][0]["annotations"] == [{"some": "annotation"}]
|
||||
|
||||
|
||||
def test_azure_ai_client_transform_input_handles_non_dict_content(mock_project_client: MagicMock) -> None:
|
||||
"""Test _transform_input_for_azure_ai handles non-dict content items."""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
# Input with string content (edge case)
|
||||
input_with_string_content = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": ["plain string content"],
|
||||
},
|
||||
]
|
||||
|
||||
result = client._transform_input_for_azure_ai(input_with_string_content) # type: ignore
|
||||
|
||||
# Should add 'type': 'message' at item level even with non-dict content
|
||||
assert result[0]["type"] == "message"
|
||||
# Non-dict content items should be preserved without modification
|
||||
assert result[0]["content"] == ["plain string content"]
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_basic(mock_project_client: MagicMock) -> None:
|
||||
"""Test prepare_options basic functionality."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
@@ -101,7 +101,7 @@ class ChatClientProtocol(Protocol):
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
@@ -160,7 +160,7 @@ class ChatClientProtocol(Protocol):
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
@@ -501,7 +501,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
@@ -596,7 +596,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
@@ -688,12 +688,18 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
chat_options: The chat options to prepare.
|
||||
"""
|
||||
chat_tool_mode = chat_options.tool_choice
|
||||
if chat_tool_mode is None or chat_tool_mode == ToolMode.NONE or chat_tool_mode == "none":
|
||||
# Explicitly disabled: clear tools and set to NONE
|
||||
if chat_tool_mode == ToolMode.NONE or chat_tool_mode == "none":
|
||||
chat_options.tools = None
|
||||
chat_options.tool_choice = ToolMode.NONE
|
||||
return
|
||||
# No tools available: set to NONE regardless of requested mode
|
||||
if not chat_options.tools:
|
||||
chat_options.tool_choice = ToolMode.NONE
|
||||
# Tools available but no explicit mode: default to AUTO
|
||||
elif chat_tool_mode is None:
|
||||
chat_options.tool_choice = ToolMode.AUTO
|
||||
# Tools available with explicit mode: preserve the mode
|
||||
else:
|
||||
chat_options.tool_choice = chat_tool_mode
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
@@ -263,8 +264,9 @@ class Executor(RequestInfoMixin, DictConvertible):
|
||||
)
|
||||
|
||||
# Invoke the handler with the message and context
|
||||
# Use deepcopy to capture original input state before handler can mutate it
|
||||
with _framework_event_origin():
|
||||
invoke_event = ExecutorInvokedEvent(self.id, message)
|
||||
invoke_event = ExecutorInvokedEvent(self.id, copy.deepcopy(message))
|
||||
await context.add_event(invoke_event)
|
||||
try:
|
||||
await handler(message, context)
|
||||
@@ -275,9 +277,11 @@ class Executor(RequestInfoMixin, DictConvertible):
|
||||
await context.add_event(failure_event)
|
||||
raise
|
||||
with _framework_event_origin():
|
||||
# Include sent messages as the completion data
|
||||
# Include sent messages and yielded outputs as the completion data
|
||||
sent_messages = context.get_sent_messages()
|
||||
completed_event = ExecutorCompletedEvent(self.id, sent_messages if sent_messages else None)
|
||||
yielded_outputs = context.get_yielded_outputs()
|
||||
completion_data = sent_messages + yielded_outputs
|
||||
completed_event = ExecutorCompletedEvent(self.id, completion_data if completion_data else None)
|
||||
await context.add_event(completed_event)
|
||||
|
||||
def _create_context_for_handler(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
import uuid
|
||||
@@ -290,6 +291,9 @@ class WorkflowContext(Generic[T_Out, T_W_Out]):
|
||||
# Track messages sent via send_message() for ExecutorCompletedEvent
|
||||
self._sent_messages: list[Any] = []
|
||||
|
||||
# Track outputs yielded via yield_output() for ExecutorCompletedEvent
|
||||
self._yielded_outputs: list[Any] = []
|
||||
|
||||
# Store trace contexts and source span IDs for linking (supporting multiple sources)
|
||||
self._trace_contexts = trace_contexts or []
|
||||
self._source_span_ids = source_span_ids or []
|
||||
@@ -336,6 +340,9 @@ class WorkflowContext(Generic[T_Out, T_W_Out]):
|
||||
output: The output to yield. This must conform to the workflow output type(s)
|
||||
declared on this context.
|
||||
"""
|
||||
# Track yielded output for ExecutorCompletedEvent (deepcopy to capture state at yield time)
|
||||
self._yielded_outputs.append(copy.deepcopy(output))
|
||||
|
||||
with _framework_event_origin():
|
||||
event = WorkflowOutputEvent(data=output, source_executor_id=self._executor_id)
|
||||
await self._runner_context.add_event(event)
|
||||
@@ -424,6 +431,14 @@ class WorkflowContext(Generic[T_Out, T_W_Out]):
|
||||
"""
|
||||
return self._sent_messages.copy()
|
||||
|
||||
def get_yielded_outputs(self) -> list[Any]:
|
||||
"""Get all outputs yielded via yield_output() during this handler execution.
|
||||
|
||||
Returns:
|
||||
A list of outputs that were yielded as workflow outputs.
|
||||
"""
|
||||
return self._yielded_outputs.copy()
|
||||
|
||||
@deprecated(
|
||||
"Override `on_checkpoint_save()` methods instead. "
|
||||
"For cross-executor state sharing, use set_shared_state() instead. "
|
||||
|
||||
@@ -321,7 +321,7 @@ def _create_otlp_exporters(
|
||||
if not actual_logs_endpoint and not actual_traces_endpoint and not actual_metrics_endpoint:
|
||||
return exporters
|
||||
|
||||
if protocol in ("grpc", "http/protobuf"):
|
||||
if protocol == "grpc":
|
||||
# Import all gRPC exporters
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as GRPCLogExporter
|
||||
@@ -357,7 +357,7 @@ def _create_otlp_exporters(
|
||||
)
|
||||
)
|
||||
|
||||
elif protocol == "http":
|
||||
elif protocol in ("http/protobuf", "http"):
|
||||
# Import all HTTP exporters
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as HTTPLogExporter
|
||||
|
||||
@@ -183,7 +183,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
translations = {
|
||||
"model_id": "model",
|
||||
"allow_multiple_tool_calls": "parallel_tool_calls",
|
||||
"max_tokens": "max_output_tokens",
|
||||
"max_tokens": "max_completion_tokens",
|
||||
}
|
||||
for old_key, new_key in translations.items():
|
||||
if old_key in run_options and old_key != new_key:
|
||||
@@ -205,8 +205,8 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
run_options.pop("tools", None)
|
||||
run_options.pop("parallel_tool_calls", None)
|
||||
run_options.pop("tool_choice", None)
|
||||
# tool choice when `tool_choice` is a dict with single key `mode`, extract the mode value
|
||||
if (tool_choice := run_options.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
# tool_choice: ToolMode serializes to {"type": "tool_mode", "mode": "..."}, extract mode
|
||||
if (tool_choice := run_options.get("tool_choice")) and isinstance(tool_choice, dict) and "mode" in tool_choice:
|
||||
run_options["tool_choice"] = tool_choice["mode"]
|
||||
|
||||
# response format
|
||||
|
||||
@@ -435,8 +435,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
else:
|
||||
run_options.pop("parallel_tool_calls", None)
|
||||
run_options.pop("tool_choice", None)
|
||||
# tool choice when `tool_choice` is a dict with single key `mode`, extract the mode value
|
||||
if (tool_choice := run_options.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
# tool_choice: ToolMode serializes to {"type": "tool_mode", "mode": "..."}, extract mode
|
||||
if (tool_choice := run_options.get("tool_choice")) and isinstance(tool_choice, dict) and "mode" in tool_choice:
|
||||
run_options["tool_choice"] = tool_choice["mode"]
|
||||
|
||||
# additional properties
|
||||
|
||||
@@ -632,3 +632,93 @@ async def test_agent_tool_receives_thread_in_kwargs(chat_client_base: Any) -> No
|
||||
assert result.text == "done"
|
||||
assert captured.get("has_thread") is True
|
||||
assert captured.get("has_message_store") is True
|
||||
|
||||
|
||||
async def test_chat_agent_tool_choice_run_level_overrides_agent_level(
|
||||
chat_client_base: Any, ai_function_tool: Any
|
||||
) -> None:
|
||||
"""Verify that tool_choice passed to run() overrides agent-level tool_choice."""
|
||||
from agent_framework import ChatOptions, ToolMode
|
||||
|
||||
captured_options: list[ChatOptions] = []
|
||||
|
||||
# Store the original inner method
|
||||
original_inner = chat_client_base._inner_get_response
|
||||
|
||||
async def capturing_inner(
|
||||
*, messages: MutableSequence[ChatMessage], chat_options: ChatOptions, **kwargs: Any
|
||||
) -> ChatResponse:
|
||||
captured_options.append(chat_options)
|
||||
return await original_inner(messages=messages, chat_options=chat_options, **kwargs)
|
||||
|
||||
chat_client_base._inner_get_response = capturing_inner
|
||||
|
||||
# Create agent with agent-level tool_choice="auto" and a tool (tools required for tool_choice to be meaningful)
|
||||
agent = ChatAgent(chat_client=chat_client_base, tool_choice="auto", tools=[ai_function_tool])
|
||||
|
||||
# Run with run-level tool_choice="required"
|
||||
await agent.run("Hello", tool_choice="required")
|
||||
|
||||
# Verify the client received tool_choice="required", not "auto"
|
||||
assert len(captured_options) >= 1
|
||||
assert captured_options[0].tool_choice == "required"
|
||||
assert captured_options[0].tool_choice == ToolMode.REQUIRED_ANY
|
||||
|
||||
|
||||
async def test_chat_agent_tool_choice_agent_level_used_when_run_level_not_specified(
|
||||
chat_client_base: Any, ai_function_tool: Any
|
||||
) -> None:
|
||||
"""Verify that agent-level tool_choice is used when run() doesn't specify one."""
|
||||
from agent_framework import ChatOptions, ToolMode
|
||||
|
||||
captured_options: list[ChatOptions] = []
|
||||
|
||||
original_inner = chat_client_base._inner_get_response
|
||||
|
||||
async def capturing_inner(
|
||||
*, messages: MutableSequence[ChatMessage], chat_options: ChatOptions, **kwargs: Any
|
||||
) -> ChatResponse:
|
||||
captured_options.append(chat_options)
|
||||
return await original_inner(messages=messages, chat_options=chat_options, **kwargs)
|
||||
|
||||
chat_client_base._inner_get_response = capturing_inner
|
||||
|
||||
# Create agent with agent-level tool_choice="required" and a tool
|
||||
agent = ChatAgent(chat_client=chat_client_base, tool_choice="required", tools=[ai_function_tool])
|
||||
|
||||
# Run without specifying tool_choice
|
||||
await agent.run("Hello")
|
||||
|
||||
# Verify the client received tool_choice="required" from agent-level
|
||||
assert len(captured_options) >= 1
|
||||
assert captured_options[0].tool_choice == "required"
|
||||
assert captured_options[0].tool_choice == ToolMode.REQUIRED_ANY
|
||||
|
||||
|
||||
async def test_chat_agent_tool_choice_none_at_run_preserves_agent_level(
|
||||
chat_client_base: Any, ai_function_tool: Any
|
||||
) -> None:
|
||||
"""Verify that tool_choice=None at run() uses agent-level default."""
|
||||
from agent_framework import ChatOptions
|
||||
|
||||
captured_options: list[ChatOptions] = []
|
||||
|
||||
original_inner = chat_client_base._inner_get_response
|
||||
|
||||
async def capturing_inner(
|
||||
*, messages: MutableSequence[ChatMessage], chat_options: ChatOptions, **kwargs: Any
|
||||
) -> ChatResponse:
|
||||
captured_options.append(chat_options)
|
||||
return await original_inner(messages=messages, chat_options=chat_options, **kwargs)
|
||||
|
||||
chat_client_base._inner_get_response = capturing_inner
|
||||
|
||||
# Create agent with agent-level tool_choice="auto" and a tool
|
||||
agent = ChatAgent(chat_client=chat_client_base, tool_choice="auto", tools=[ai_function_tool])
|
||||
|
||||
# Run with explicitly passing None (same as not specifying)
|
||||
await agent.run("Hello", tool_choice=None)
|
||||
|
||||
# Verify the client received tool_choice="auto" from agent-level
|
||||
assert len(captured_options) >= 1
|
||||
assert captured_options[0].tool_choice == "auto"
|
||||
|
||||
@@ -844,6 +844,54 @@ def test_chat_options_and(ai_function_tool, ai_tool) -> None:
|
||||
assert options3.additional_properties.get("p") == 1
|
||||
|
||||
|
||||
def test_chat_options_and_tool_choice_override() -> None:
|
||||
"""Test that tool_choice from other takes precedence in ChatOptions merge."""
|
||||
# Agent-level defaults to "auto"
|
||||
agent_options = ChatOptions(model_id="gpt-4o", tool_choice="auto")
|
||||
# Run-level specifies "required"
|
||||
run_options = ChatOptions(tool_choice="required")
|
||||
|
||||
merged = agent_options & run_options
|
||||
|
||||
# Run-level should override agent-level
|
||||
assert merged.tool_choice == "required"
|
||||
assert merged.model_id == "gpt-4o" # Other fields preserved
|
||||
|
||||
|
||||
def test_chat_options_and_tool_choice_none_in_other_uses_self() -> None:
|
||||
"""Test that when other.tool_choice is None, self.tool_choice is used."""
|
||||
agent_options = ChatOptions(tool_choice="auto")
|
||||
run_options = ChatOptions(model_id="gpt-4.1") # tool_choice is None
|
||||
|
||||
merged = agent_options & run_options
|
||||
|
||||
# Should keep agent-level tool_choice since run-level is None
|
||||
assert merged.tool_choice == "auto"
|
||||
assert merged.model_id == "gpt-4.1"
|
||||
|
||||
|
||||
def test_chat_options_and_tool_choice_with_tool_mode() -> None:
|
||||
"""Test ChatOptions merge with ToolMode objects."""
|
||||
agent_options = ChatOptions(tool_choice=ToolMode.AUTO)
|
||||
run_options = ChatOptions(tool_choice=ToolMode.REQUIRED_ANY)
|
||||
|
||||
merged = agent_options & run_options
|
||||
|
||||
assert merged.tool_choice == ToolMode.REQUIRED_ANY
|
||||
assert merged.tool_choice == "required" # ToolMode equality with string
|
||||
|
||||
|
||||
def test_chat_options_and_tool_choice_required_specific_function() -> None:
|
||||
"""Test ChatOptions merge with required specific function."""
|
||||
agent_options = ChatOptions(tool_choice="auto")
|
||||
run_options = ChatOptions(tool_choice=ToolMode.REQUIRED(function_name="get_weather"))
|
||||
|
||||
merged = agent_options & run_options
|
||||
|
||||
assert merged.tool_choice == "required"
|
||||
assert merged.tool_choice.required_function_name == "get_weather"
|
||||
|
||||
|
||||
# region Agent Response Fixtures
|
||||
|
||||
|
||||
|
||||
@@ -236,6 +236,36 @@ async def test_openai_chat_completion_response() -> None:
|
||||
assert "scientists" in response.text
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_chat_completion_response_params() -> None:
|
||||
"""Test OpenAI chat completion responses."""
|
||||
openai_chat_client = OpenAIChatClient()
|
||||
|
||||
assert isinstance(openai_chat_client, ChatClientProtocol)
|
||||
|
||||
messages: list[ChatMessage] = []
|
||||
messages.append(
|
||||
ChatMessage(
|
||||
role="user",
|
||||
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
|
||||
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
|
||||
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
|
||||
"of climate change.",
|
||||
)
|
||||
)
|
||||
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = await openai_chat_client.get_response(
|
||||
messages=messages, chat_options=ChatOptions(max_tokens=150, temperature=0.7, top_p=0.9)
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert "scientists" in response.text
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_chat_completion_response_tools() -> None:
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import pytest
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
Executor,
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorInvokedEvent,
|
||||
Message,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
executor,
|
||||
handler,
|
||||
)
|
||||
|
||||
@@ -182,8 +184,8 @@ async def test_executor_completed_event_contains_sent_messages():
|
||||
assert collector_completed.data is None
|
||||
|
||||
|
||||
async def test_executor_completed_event_none_when_no_messages_sent():
|
||||
"""Test that ExecutorCompletedEvent.data is None when no messages are sent."""
|
||||
async def test_executor_completed_event_includes_yielded_outputs():
|
||||
"""Test that ExecutorCompletedEvent.data includes yielded outputs."""
|
||||
from typing_extensions import Never
|
||||
|
||||
from agent_framework import WorkflowOutputEvent
|
||||
@@ -201,9 +203,10 @@ async def test_executor_completed_event_none_when_no_messages_sent():
|
||||
|
||||
assert len(completed_events) == 1
|
||||
assert completed_events[0].executor_id == "yielder"
|
||||
assert completed_events[0].data is None
|
||||
# Yielded outputs are now included in ExecutorCompletedEvent.data
|
||||
assert completed_events[0].data == ["TEST"]
|
||||
|
||||
# Verify the output was still yielded correctly
|
||||
# Verify the output was also yielded as WorkflowOutputEvent
|
||||
output_events = [e for e in events if isinstance(e, WorkflowOutputEvent)]
|
||||
assert len(output_events) == 1
|
||||
assert output_events[0].data == "TEST"
|
||||
@@ -261,3 +264,35 @@ async def test_executor_events_with_complex_message_types():
|
||||
collector_invoked = next(e for e in invoked_events if e.executor_id == "collector")
|
||||
assert isinstance(collector_invoked.data, Response)
|
||||
assert collector_invoked.data.results == ["HELLO", "HELLO", "HELLO"]
|
||||
|
||||
|
||||
async def test_executor_invoked_event_data_not_mutated_by_handler():
|
||||
"""Test that ExecutorInvokedEvent.data captures original input, not mutated input."""
|
||||
|
||||
@executor(id="Mutator")
|
||||
async def mutator(messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
# The handler mutates the input list by appending new messages
|
||||
original_len = len(messages)
|
||||
messages.append(ChatMessage(role="assistant", text="Added by executor"))
|
||||
await ctx.send_message(messages)
|
||||
# Verify mutation happened
|
||||
assert len(messages) == original_len + 1
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(mutator).build()
|
||||
|
||||
# Run with a single user message
|
||||
input_messages = [ChatMessage(role="user", text="hello")]
|
||||
events = await workflow.run(input_messages)
|
||||
|
||||
# Find the invoked event for the Mutator executor
|
||||
invoked_events = [e for e in events if isinstance(e, ExecutorInvokedEvent)]
|
||||
assert len(invoked_events) == 1
|
||||
mutator_invoked = invoked_events[0]
|
||||
|
||||
# The event data should contain ONLY the original input (1 user message)
|
||||
assert mutator_invoked.executor_id == "Mutator"
|
||||
assert len(mutator_invoked.data) == 1, (
|
||||
f"Expected 1 message (original input), got {len(mutator_invoked.data)}: "
|
||||
f"{[m.text for m in mutator_invoked.data]}"
|
||||
)
|
||||
assert mutator_invoked.data[0].text == "hello"
|
||||
|
||||
@@ -28,7 +28,11 @@ async def main() -> None:
|
||||
tools={
|
||||
"type": "sharepoint_grounding_preview",
|
||||
"sharepoint_grounding_preview": {
|
||||
"project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"]
|
||||
"project_connections": [
|
||||
{
|
||||
"project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"],
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
) as agent,
|
||||
|
||||
@@ -119,6 +119,7 @@ async def main() -> None:
|
||||
Input: str: 'HELLO WORLD'
|
||||
[WORKFLOW OUTPUT] str: 'DLROW OLLEH'
|
||||
[COMPLETED] reverse_text
|
||||
Output: list: [str: 'DLROW OLLEH']
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user