mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
8
Commits
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
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 about Agent Framework.
|
||||
@@ -0,0 +1,70 @@
|
||||
name: .NET Bug Report
|
||||
description: Report a bug in the Agent Framework .NET SDK
|
||||
title: ".NET: [Bug]: "
|
||||
labels: ["bug", ".NET"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the bug.
|
||||
placeholder: |
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue.
|
||||
placeholder: |
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: error-messages
|
||||
attributes:
|
||||
label: Error Messages / Stack Traces
|
||||
description: Include any error messages or stack traces you received.
|
||||
placeholder: |
|
||||
```
|
||||
Paste error messages or stack traces here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-packages
|
||||
attributes:
|
||||
label: Package Versions
|
||||
description: List the Microsoft.Agents.* packages and versions you are using
|
||||
placeholder: "e.g., Microsoft.Agents.AI.Abstractions: 1.0.0, Microsoft.Agents.AI.OpenAI: 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: dotnet-version
|
||||
attributes:
|
||||
label: .NET Version
|
||||
description: What version of .NET are you using?
|
||||
placeholder: "e.g., .NET 8.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots that might be helpful.
|
||||
placeholder: "Any additional information..."
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Feature Request
|
||||
description: Request a new feature for Microsoft Agent Framework
|
||||
title: "[Feature]: "
|
||||
type: feature
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please describe the feature you'd like and why it would be useful.
|
||||
placeholder: |
|
||||
Describe the feature you're requesting:
|
||||
- What problem does it solve?
|
||||
- What would the expected behavior be?
|
||||
- Are there any alternatives you've considered?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a code sample showing how you'd like to use this feature.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: language
|
||||
attributes:
|
||||
label: Language/SDK
|
||||
description: Which language/SDK does this feature apply to?
|
||||
options:
|
||||
- Both
|
||||
- .NET
|
||||
- Python
|
||||
- Other / Not Applicable
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Python Bug Report
|
||||
description: Report a bug in the Agent Framework Python SDK
|
||||
title: "Python: [Bug]: "
|
||||
labels: ["bug", "Python"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the bug.
|
||||
placeholder: |
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: error-messages
|
||||
attributes:
|
||||
label: Error Messages / Stack Traces
|
||||
description: Include any error messages or stack traces you received.
|
||||
placeholder: |
|
||||
```
|
||||
Paste error messages or stack traces here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-packages
|
||||
attributes:
|
||||
label: Package Versions
|
||||
description: List the agent-framework-* packages and versions you are using
|
||||
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: python-version
|
||||
attributes:
|
||||
label: Python Version
|
||||
description: What version of Python are you using?
|
||||
placeholder: "e.g., Python 3.11"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots 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)
|
||||
|
||||
+6
-5
@@ -208,13 +208,14 @@ WARP.md
|
||||
**/projectBrief.md
|
||||
|
||||
# Azurite storage emulator files
|
||||
*/__azurite_db_blob__.json
|
||||
*/__azurite_db_blob_extent__.json
|
||||
*/__azurite_db_queue__.json
|
||||
*/__azurite_db_queue_extent__.json
|
||||
*/__azurite_db_table__.json
|
||||
*/__azurite_db_blob__.json*
|
||||
*/__azurite_db_blob_extent__.json*
|
||||
*/__azurite_db_queue__.json*
|
||||
*/__azurite_db_queue_extent__.json*
|
||||
*/__azurite_db_table__.json*
|
||||
*/__blobstorage__/
|
||||
*/__queuestorage__/
|
||||
*/AzuriteConfig
|
||||
|
||||
# Azure Functions local settings
|
||||
local.settings.json
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.0.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.0.1" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<!-- Google Gemini -->
|
||||
<PackageVersion Include="Google.GenAI" Version="0.6.0" />
|
||||
<PackageVersion Include="Google.GenAI" Version="0.9.0" />
|
||||
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
||||
<!-- Microsoft.Azure.* -->
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
|
||||
@@ -100,7 +100,7 @@
|
||||
<!-- MCP -->
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.8.0" />
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251219.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251219.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251219.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260108.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260108.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260108.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
|
||||
@@ -9,7 +9,7 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
@@ -25,7 +25,7 @@ AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAd
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgent",
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
|
||||
@@ -8,9 +8,8 @@ using Azure.AI.AgentServer.AgentFramework.Extensions;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Data;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
@@ -28,7 +27,10 @@ AIAgent agent = new AzureOpenAIClient(
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
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(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251125.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251219.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Microsoft.Agents.AI.A2A;
|
||||
/// Support for tasks will be added later as part of the long-running
|
||||
/// executions work.
|
||||
/// </remarks>
|
||||
internal sealed class A2AAgent : AIAgent
|
||||
public sealed class A2AAgent : AIAgent
|
||||
{
|
||||
private readonly A2AClient _a2aClient;
|
||||
private readonly string? _id;
|
||||
@@ -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);
|
||||
|
||||
@@ -84,9 +84,13 @@ internal sealed class A2AAgent : AIAgent
|
||||
}
|
||||
else
|
||||
{
|
||||
var a2aMessage = CreateA2AMessage(typedThread, messages);
|
||||
MessageSendParams sendParams = new()
|
||||
{
|
||||
Message = CreateA2AMessage(typedThread, messages),
|
||||
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
|
||||
a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
|
||||
@@ -131,7 +135,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);
|
||||
|
||||
@@ -154,9 +158,13 @@ internal sealed class A2AAgent : AIAgent
|
||||
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var a2aMessage = CreateA2AMessage(typedThread, messages);
|
||||
MessageSendParams sendParams = new()
|
||||
{
|
||||
Message = CreateA2AMessage(typedThread, messages),
|
||||
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
|
||||
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
|
||||
|
||||
@@ -198,10 +206,10 @@ internal sealed class A2AAgent : AIAgent
|
||||
protected override string? IdCore => this._id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Name => this._name ?? base.Name;
|
||||
public override string? Name => this._name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Description => this._description ?? base.Description;
|
||||
public override string? Description => this._description;
|
||||
|
||||
private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ internal static class A2AMetadataExtensions
|
||||
/// <summary>
|
||||
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can be replaced by the one from A2A SDK once it is public.
|
||||
/// </remarks>
|
||||
/// <param name="metadata">The metadata dictionary to convert.</param>
|
||||
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
|
||||
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for AdditionalPropertiesDictionary.
|
||||
/// </summary>
|
||||
internal static class AdditionalPropertiesDictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can be replaced by the one from A2A SDK once it is available.
|
||||
/// </remarks>
|
||||
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
|
||||
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
|
||||
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
|
||||
{
|
||||
if (additionalProperties is not { Count: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var metadata = new Dictionary<string, JsonElement>();
|
||||
|
||||
foreach (var kvp in additionalProperties)
|
||||
{
|
||||
if (kvp.Value is JsonElement)
|
||||
{
|
||||
metadata[kvp.Key] = (JsonElement)kvp.Value!;
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -291,6 +291,15 @@ public class AgentRunResponse
|
||||
return updates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the response text into the given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <returns>The result as the requested type.</returns>
|
||||
/// <exception cref="InvalidOperationException">The result is not parsable into the requested type.</exception>
|
||||
public T Deserialize<T>() =>
|
||||
this.Deserialize<T>(AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the response text into the given type using the specified serializer options.
|
||||
/// </summary>
|
||||
@@ -311,6 +320,15 @@ public class AgentRunResponse
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to deserialize response text into the given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <param name="structuredOutput">The parsed structured output.</param>
|
||||
/// <returns><see langword="true" /> if parsing was successful; otherwise, <see langword="false" />.</returns>
|
||||
public bool TryDeserialize<T>([NotNullWhen(true)] out T? structuredOutput) =>
|
||||
this.TryDeserialize(AgentAbstractionsJsonUtilities.DefaultOptions, out structuredOutput);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to deserialize response text into the given type using the specified serializer options.
|
||||
/// </summary>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -43,10 +43,14 @@ public static class AIAgentExtensions
|
||||
{
|
||||
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
var options = messageSendParams.Metadata is not { Count: > 0 }
|
||||
? null
|
||||
: new AgentRunOptions { AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
messageSendParams.ToChatMessages(),
|
||||
thread: thread,
|
||||
options: options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false);
|
||||
@@ -56,7 +60,8 @@ public static class AIAgentExtensions
|
||||
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
ContextId = contextId,
|
||||
Role = MessageRole.Agent,
|
||||
Parts = parts
|
||||
Parts = parts,
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for A2A metadata dictionary.
|
||||
/// </summary>
|
||||
internal static class A2AMetadataExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can be replaced by the one from A2A SDK once it is public.
|
||||
/// </remarks>
|
||||
/// <param name="metadata">The metadata dictionary to convert.</param>
|
||||
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
|
||||
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
{
|
||||
if (metadata is not { Count: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var additionalProperties = new AdditionalPropertiesDictionary();
|
||||
foreach (var kvp in metadata)
|
||||
{
|
||||
additionalProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return additionalProperties;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for AdditionalPropertiesDictionary.
|
||||
/// </summary>
|
||||
internal static class AdditionalPropertiesDictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can be replaced by the one from A2A SDK once it is available.
|
||||
/// </remarks>
|
||||
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
|
||||
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
|
||||
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
|
||||
{
|
||||
if (additionalProperties is not { Count: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var metadata = new Dictionary<string, JsonElement>();
|
||||
|
||||
foreach (var kvp in additionalProperties)
|
||||
{
|
||||
if (kvp.Value is JsonElement)
|
||||
{
|
||||
metadata[kvp.Key] = (JsonElement)kvp.Value!;
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -50,7 +50,7 @@ TokenCredential browserCredential = new InteractiveBrowserCredential(
|
||||
IChatClient client = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.WithPurview(browserCredential, new PurviewSettings("My Sample App"))
|
||||
@@ -198,7 +198,7 @@ Use the chat middleware when you attach directly to a chat client (e.g. minimal
|
||||
IChatClient client = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.WithPurview(browserCredential, new PurviewSettings("Agent Framework Test App"))
|
||||
|
||||
@@ -10,4 +10,10 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// <param name="e">
|
||||
/// Optionally, the <see cref="Exception"/> representing the error.
|
||||
/// </param>
|
||||
public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e);
|
||||
public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the exception that caused the current operation to fail, if one occurred.
|
||||
/// </summary>
|
||||
public Exception? Exception => this.Data as Exception;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,12 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
private readonly string? _id;
|
||||
private readonly CheckpointManager? _checkpointManager;
|
||||
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
|
||||
private readonly bool _includeExceptionDetails;
|
||||
private readonly Task<ProtocolDescriptor> _describeTask;
|
||||
|
||||
private readonly ConcurrentDictionary<string, string> _assignedRunIds = [];
|
||||
|
||||
public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null)
|
||||
public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null, bool includeExceptionDetails = false)
|
||||
{
|
||||
this._workflow = Throw.IfNull(workflow);
|
||||
|
||||
@@ -30,6 +31,7 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
? InProcessExecution.Concurrent
|
||||
: InProcessExecution.OffThread);
|
||||
this._checkpointManager = checkpointManager;
|
||||
this._includeExceptionDetails = includeExceptionDetails;
|
||||
|
||||
this._id = id;
|
||||
this.Name = name;
|
||||
@@ -61,12 +63,12 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
protocol.ThrowIfNotChatProtocol();
|
||||
}
|
||||
|
||||
public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager);
|
||||
public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails);
|
||||
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, jsonSerializerOptions);
|
||||
=> new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, 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 +77,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 +105,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,
|
||||
|
||||
@@ -21,6 +21,8 @@ public static class WorkflowHostingExtensions
|
||||
/// <param name="executionEnvironment">Specify the execution environment to use when running the workflows. See
|
||||
/// <see cref="InProcessExecution.OffThread"/>, <see cref="InProcessExecution.Concurrent"/> and
|
||||
/// <see cref="InProcessExecution.Lockstep"/> for the in-process environments.</param>
|
||||
/// <param name="includeExceptionDetails">If <see langword="true"/>, will include <see cref="System.Exception.Message"/>
|
||||
/// in the <see cref="ErrorContent"/> representing the workflow error.</param>
|
||||
/// <returns></returns>
|
||||
public static AIAgent AsAgent(
|
||||
this Workflow workflow,
|
||||
@@ -28,9 +30,10 @@ public static class WorkflowHostingExtensions
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
CheckpointManager? checkpointManager = null,
|
||||
IWorkflowExecutionEnvironment? executionEnvironment = null)
|
||||
IWorkflowExecutionEnvironment? executionEnvironment = null,
|
||||
bool includeExceptionDetails = false)
|
||||
{
|
||||
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment);
|
||||
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment, includeExceptionDetails);
|
||||
}
|
||||
|
||||
internal static FunctionCallContent ToFunctionCall(this ExternalRequest request)
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -17,14 +18,16 @@ internal sealed class WorkflowThread : AgentThread
|
||||
{
|
||||
private readonly Workflow _workflow;
|
||||
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
|
||||
private readonly bool _includeExceptionDetails;
|
||||
|
||||
private readonly CheckpointManager _checkpointManager;
|
||||
private readonly InMemoryCheckpointManager? _inMemoryCheckpointManager;
|
||||
|
||||
public WorkflowThread(Workflow workflow, string runId, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null)
|
||||
public WorkflowThread(Workflow workflow, string runId, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false)
|
||||
{
|
||||
this._workflow = Throw.IfNull(workflow);
|
||||
this._executionEnvironment = Throw.IfNull(executionEnvironment);
|
||||
this._includeExceptionDetails = includeExceptionDetails;
|
||||
|
||||
// If the user provided an external checkpoint manager, use that, otherwise rely on an in-memory one.
|
||||
// TODO: Implement persist-only-last functionality for in-memory checkpoint manager, to avoid unbounded
|
||||
@@ -35,7 +38,7 @@ internal sealed class WorkflowThread : AgentThread
|
||||
this.MessageStore = new WorkflowMessageStore();
|
||||
}
|
||||
|
||||
public WorkflowThread(Workflow workflow, JsonElement serializedThread, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public WorkflowThread(Workflow workflow, JsonElement serializedThread, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
this._workflow = Throw.IfNull(workflow);
|
||||
this._executionEnvironment = Throw.IfNull(executionEnvironment);
|
||||
@@ -80,7 +83,7 @@ internal sealed class WorkflowThread : AgentThread
|
||||
return marshaller.Marshal(info);
|
||||
}
|
||||
|
||||
public AgentRunResponseUpdate CreateUpdate(string responseId, params AIContent[] parts)
|
||||
public AgentRunResponseUpdate CreateUpdate(string responseId, object raw, params AIContent[] parts)
|
||||
{
|
||||
Throw.IfNullOrEmpty(parts);
|
||||
|
||||
@@ -89,7 +92,8 @@ internal sealed class WorkflowThread : AgentThread
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Role = ChatRole.Assistant,
|
||||
ResponseId = responseId
|
||||
ResponseId = responseId,
|
||||
RawRepresentation = raw
|
||||
};
|
||||
|
||||
this.MessageStore.AddMessages(update.ToChatMessage());
|
||||
@@ -153,10 +157,29 @@ internal sealed class WorkflowThread : AgentThread
|
||||
|
||||
case RequestInfoEvent requestInfo:
|
||||
FunctionCallContent fcContent = requestInfo.Request.ToFunctionCall();
|
||||
AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, fcContent);
|
||||
AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, evt, fcContent);
|
||||
yield return update;
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
Exception? exception = workflowError.Exception;
|
||||
if (exception is TargetInvocationException tie && tie.InnerException != null)
|
||||
{
|
||||
exception = tie.InnerException;
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
string message = this._includeExceptionDetails
|
||||
? exception.Message
|
||||
: "An error occurred while executing the workflow.";
|
||||
|
||||
ErrorContent errorContent = new(message);
|
||||
yield return this.CreateUpdate(this.LastResponseId, evt, errorContent);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SuperStepCompletedEvent stepCompleted:
|
||||
this.LastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint;
|
||||
goto default;
|
||||
|
||||
@@ -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,
|
||||
@@ -162,7 +162,10 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
|
||||
static AgentRunResponse CreateResponse(ChatResponse chatResponse)
|
||||
{
|
||||
return new AgentRunResponse(chatResponse);
|
||||
return new AgentRunResponse(chatResponse)
|
||||
{
|
||||
ContinuationToken = WrapContinuationToken(chatResponse.ContinuationToken)
|
||||
};
|
||||
}
|
||||
|
||||
return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, cancellationToken);
|
||||
@@ -193,7 +196,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,11 +204,14 @@ 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,
|
||||
ChatClientAgentContinuationToken? continuationToken) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ValidateStreamResumptionAllowed(chatOptions?.ContinuationToken, safeThread);
|
||||
|
||||
var chatClient = this.ChatClient;
|
||||
|
||||
chatClient = ApplyRunOptionsTransformations(options, chatClient);
|
||||
@@ -214,7 +220,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
|
||||
this._logger.LogAgentChatClientInvokingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType);
|
||||
|
||||
List<ChatResponseUpdate> responseUpdates = [];
|
||||
List<ChatResponseUpdate> responseUpdates = GetResponseUpdates(continuationToken);
|
||||
|
||||
IAsyncEnumerator<ChatResponseUpdate> responseUpdatesEnumerator;
|
||||
|
||||
@@ -225,7 +231,8 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -239,7 +246,8 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -251,7 +259,12 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
update.AuthorName ??= this.Name;
|
||||
|
||||
responseUpdates.Add(update);
|
||||
yield return new(update) { AgentId = this.Id };
|
||||
|
||||
yield return new(update)
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ContinuationToken = WrapContinuationToken(update.ContinuationToken, GetInputMessages(inputMessages, continuationToken), responseUpdates)
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
@@ -260,7 +273,8 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -272,10 +286,10 @@ 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, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, GetInputMessages(inputMessages, continuationToken), aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -379,7 +393,12 @@ 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,
|
||||
ChatClientAgentContinuationToken? _) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, inputMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var chatClient = this.ChatClient;
|
||||
@@ -398,6 +417,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 +435,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);
|
||||
@@ -470,20 +490,20 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// <param name="runOptions">Optional run options that may include specific chat configuration settings.</param>
|
||||
/// <returns>A <see cref="ChatOptions"/> object representing the merged chat configuration, or <see langword="null"/> if
|
||||
/// neither the run options nor the agent's chat options are available.</returns>
|
||||
private ChatOptions? CreateConfiguredChatOptions(AgentRunOptions? runOptions)
|
||||
private (ChatOptions?, ChatClientAgentContinuationToken?) CreateConfiguredChatOptions(AgentRunOptions? runOptions)
|
||||
{
|
||||
ChatOptions? requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone();
|
||||
|
||||
// If no agent chat options were provided, return the request chat options as is.
|
||||
if (this._agentOptions?.ChatOptions is null)
|
||||
{
|
||||
return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
|
||||
return GetContinuationTokenAndApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
|
||||
}
|
||||
|
||||
// If no request chat options were provided, use the agent's chat options clone.
|
||||
if (requestChatOptions is null)
|
||||
{
|
||||
return ApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions);
|
||||
return GetContinuationTokenAndApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions);
|
||||
}
|
||||
|
||||
// If both are present, we need to merge them.
|
||||
@@ -579,19 +599,26 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
return ApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
|
||||
return GetContinuationTokenAndApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
|
||||
|
||||
static ChatOptions? ApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
|
||||
static (ChatOptions?, ChatClientAgentContinuationToken?) GetContinuationTokenAndApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
|
||||
{
|
||||
// If any of the background response properties are set in the run options, we should apply both to the chat options.
|
||||
if (agentRunOptions?.AllowBackgroundResponses is not null || agentRunOptions?.ContinuationToken is not null)
|
||||
if (agentRunOptions?.AllowBackgroundResponses is not null)
|
||||
{
|
||||
chatOptions ??= new ChatOptions();
|
||||
chatOptions.AllowBackgroundResponses = agentRunOptions.AllowBackgroundResponses;
|
||||
chatOptions.ContinuationToken = agentRunOptions.ContinuationToken;
|
||||
}
|
||||
|
||||
return chatOptions;
|
||||
ChatClientAgentContinuationToken? agentContinuationToken = null;
|
||||
|
||||
if ((agentRunOptions?.ContinuationToken ?? chatOptions?.ContinuationToken) is { } continuationToken)
|
||||
{
|
||||
agentContinuationToken = ChatClientAgentContinuationToken.FromToken(continuationToken);
|
||||
chatOptions ??= new ChatOptions();
|
||||
chatOptions.ContinuationToken = agentContinuationToken!.InnerToken;
|
||||
}
|
||||
|
||||
return (chatOptions, agentContinuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,14 +629,22 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
/// <param name="inputMessages">The input messages to use.</param>
|
||||
/// <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(
|
||||
/// <returns>A tuple containing the thread, chat options, messages and continuation token.</returns>
|
||||
private async Task
|
||||
<(
|
||||
ChatClientAgentThread AgentThread,
|
||||
ChatOptions? ChatOptions,
|
||||
List<ChatMessage> InputMessagesForChatClient,
|
||||
IList<ChatMessage>? AIContextProviderMessages,
|
||||
IList<ChatMessage>? ChatMessageStoreMessages,
|
||||
ChatClientAgentContinuationToken? ContinuationToken
|
||||
)> PrepareThreadAndMessagesAsync(
|
||||
AgentThread? thread,
|
||||
IEnumerable<ChatMessage> inputMessages,
|
||||
AgentRunOptions? runOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions);
|
||||
(ChatOptions? chatOptions, ChatClientAgentContinuationToken? continuationToken) = this.CreateConfiguredChatOptions(runOptions);
|
||||
|
||||
// Supplying a thread for background responses is required to prevent inconsistent experience
|
||||
// for callers if they forget to provide the thread for initial or follow-up runs.
|
||||
@@ -630,13 +665,9 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
|
||||
}
|
||||
|
||||
if (chatOptions?.ContinuationToken is not null && typedThread.ConversationId is null && typedThread.MessageStore is null)
|
||||
{
|
||||
throw new InvalidOperationException("Continuation tokens are not allowed to be used for initial runs.");
|
||||
}
|
||||
|
||||
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 +675,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 +712,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 +732,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
chatOptions.ConversationId = typedThread.ConversationId;
|
||||
}
|
||||
|
||||
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages);
|
||||
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages, chatMessageStoreMessages, continuationToken);
|
||||
}
|
||||
|
||||
private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread thread, string? responseConversationId)
|
||||
@@ -725,7 +759,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,32 +773,80 @@ 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 void ValidateStreamResumptionAllowed(ResponseContinuationToken? continuationToken, ChatClientAgentThread safeThread)
|
||||
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;
|
||||
}
|
||||
|
||||
private static ChatClientAgentContinuationToken? WrapContinuationToken(ResponseContinuationToken? continuationToken, IEnumerable<ChatMessage>? inputMessages = null, List<ChatResponseUpdate>? responseUpdates = null)
|
||||
{
|
||||
if (continuationToken is null)
|
||||
{
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Streaming resumption is only supported with chat history managed by the agent service because, currently, there's no good solution
|
||||
// to collect updates received in failed runs and pass them to the last successful run so it can store them to the message store.
|
||||
if (safeThread.ConversationId is null)
|
||||
return new(continuationToken)
|
||||
{
|
||||
throw new NotSupportedException("Streaming resumption is only supported when chat history is stored and managed by the underlying AI service.");
|
||||
// Save input messages to the continuation token so they can be added to the thread and
|
||||
// provided to the context provider in the last successful streaming resumption run.
|
||||
// That's necessary for scenarios where initial streaming run is interrupted and streaming is resumed later.
|
||||
InputMessages = inputMessages?.Any() is true ? inputMessages : null,
|
||||
|
||||
// Save all updates received so far to the continuation token so they can be provided to the
|
||||
// message store and context provider in the last successful streaming resumption run.
|
||||
// That's necessary for scenarios where a streaming run is interrupted after some updates were received.
|
||||
ResponseUpdates = responseUpdates?.Count > 0 ? responseUpdates : null
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<ChatMessage> GetInputMessages(IReadOnlyCollection<ChatMessage> inputMessages, ChatClientAgentContinuationToken? token)
|
||||
{
|
||||
// First, use input messages if provided.
|
||||
if (inputMessages.Count > 0)
|
||||
{
|
||||
return inputMessages;
|
||||
}
|
||||
|
||||
// Similarly, streaming resumption is not supported when a context provider is used because, currently, there's no good solution
|
||||
// to collect updates received in failed runs and pass them to the last successful run so it can notify the context provider of the updates.
|
||||
if (safeThread.AIContextProvider is not null)
|
||||
{
|
||||
throw new NotSupportedException("Using context provider with streaming resumption is not supported.");
|
||||
}
|
||||
// Fallback to messages saved in the continuation token if available.
|
||||
return token?.InputMessages ?? [];
|
||||
}
|
||||
|
||||
private static List<ChatResponseUpdate> GetResponseUpdates(ChatClientAgentContinuationToken? token)
|
||||
{
|
||||
// Restore any previously received updates from the continuation token.
|
||||
return token?.ResponseUpdates?.ToList() ?? [];
|
||||
}
|
||||
|
||||
private string GetLoggingAgentName() => this.Name ?? "UnnamedAgent";
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a continuation token for ChatClientAgent operations.
|
||||
/// </summary>
|
||||
internal class ChatClientAgentContinuationToken : ResponseContinuationToken
|
||||
{
|
||||
private const string TokenTypeName = "chatClientAgentContinuationToken";
|
||||
private const string TypeDiscriminator = "type";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentContinuationToken"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerToken">A continuation token provided by the underlying <see cref="IChatClient"/>.</param>
|
||||
[JsonConstructor]
|
||||
internal ChatClientAgentContinuationToken(ResponseContinuationToken innerToken)
|
||||
{
|
||||
this.InnerToken = innerToken;
|
||||
}
|
||||
|
||||
public override ReadOnlyMemory<byte> ToBytes()
|
||||
{
|
||||
using MemoryStream stream = new();
|
||||
using Utf8JsonWriter writer = new(stream);
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
// This property should be the first one written to identify the type during deserialization.
|
||||
writer.WriteString(TypeDiscriminator, TokenTypeName);
|
||||
|
||||
writer.WriteString("innerToken", JsonSerializer.Serialize(this.InnerToken, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken))));
|
||||
|
||||
if (this.InputMessages?.Any() is true)
|
||||
{
|
||||
writer.WriteString("inputMessages", JsonSerializer.Serialize(this.InputMessages, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IEnumerable<ChatMessage>))));
|
||||
}
|
||||
|
||||
if (this.ResponseUpdates?.Count > 0)
|
||||
{
|
||||
writer.WriteString("responseUpdates", JsonSerializer.Serialize(this.ResponseUpdates, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IReadOnlyList<ChatResponseUpdate>))));
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
|
||||
writer.Flush();
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="ChatClientAgentContinuationToken"/> from the provided <paramref name="token"/>.
|
||||
/// </summary>
|
||||
/// <param name="token">The token to create the <see cref="ChatClientAgentContinuationToken"/> from.</param>
|
||||
/// <returns>A <see cref="ChatClientAgentContinuationToken"/> equivalent of the provided <paramref name="token"/>.</returns>
|
||||
internal static ChatClientAgentContinuationToken FromToken(ResponseContinuationToken token)
|
||||
{
|
||||
if (token is ChatClientAgentContinuationToken chatClientContinuationToken)
|
||||
{
|
||||
return chatClientContinuationToken;
|
||||
}
|
||||
|
||||
ReadOnlyMemory<byte> data = token.ToBytes();
|
||||
|
||||
if (data.Length == 0)
|
||||
{
|
||||
Throw.ArgumentException(nameof(token), "Failed to create ChatClientAgentContinuationToken from provided token because it does not contain any data.");
|
||||
}
|
||||
|
||||
Utf8JsonReader reader = new(data.Span);
|
||||
|
||||
// Move to the start object token.
|
||||
_ = reader.Read();
|
||||
|
||||
// Validate that the token is of this type.
|
||||
ValidateTokenType(reader, token);
|
||||
|
||||
ResponseContinuationToken? innerToken = null;
|
||||
IEnumerable<ChatMessage>? inputMessages = null;
|
||||
IReadOnlyList<ChatResponseUpdate>? responseUpdates = null;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
switch (reader.GetString())
|
||||
{
|
||||
case "innerToken":
|
||||
_ = reader.Read();
|
||||
var innerTokenJson = reader.GetString() ?? throw new ArgumentException("No content for innerToken property.", nameof(token));
|
||||
innerToken = (ResponseContinuationToken?)JsonSerializer.Deserialize(innerTokenJson, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
|
||||
break;
|
||||
case "inputMessages":
|
||||
_ = reader.Read();
|
||||
var innerMessagesJson = reader.GetString() ?? throw new ArgumentException("No content for inputMessages property.", nameof(token));
|
||||
inputMessages = (IEnumerable<ChatMessage>?)JsonSerializer.Deserialize(innerMessagesJson, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IEnumerable<ChatMessage>)));
|
||||
break;
|
||||
case "responseUpdates":
|
||||
_ = reader.Read();
|
||||
var responseUpdatesJson = reader.GetString() ?? throw new ArgumentException("No content for responseUpdates property.", nameof(token));
|
||||
responseUpdates = (IReadOnlyList<ChatResponseUpdate>?)JsonSerializer.Deserialize(responseUpdatesJson, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IReadOnlyList<ChatResponseUpdate>)));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (innerToken is null)
|
||||
{
|
||||
Throw.ArgumentException(nameof(token), "Failed to create ChatClientAgentContinuationToken from provided token because it does not contain an inner token.");
|
||||
}
|
||||
|
||||
return new ChatClientAgentContinuationToken(innerToken)
|
||||
{
|
||||
InputMessages = inputMessages,
|
||||
ResponseUpdates = responseUpdates
|
||||
};
|
||||
}
|
||||
|
||||
private static void ValidateTokenType(Utf8JsonReader reader, ResponseContinuationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Move to the first property.
|
||||
_ = reader.Read();
|
||||
|
||||
// If the first property name is not "type", or its value does not match this token type name, then we know its not this token type.
|
||||
if (reader.GetString() != TypeDiscriminator || !reader.Read() || reader.GetString() != TokenTypeName)
|
||||
{
|
||||
Throw.ArgumentException(nameof(token), "Failed to create ChatClientAgentContinuationToken from provided token because it is not of the correct type.");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Throw.ArgumentException(nameof(token), "Failed to create ChatClientAgentContinuationToken from provided token because it could not be parsed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a continuation token provided by the underlying <see cref="IChatClient"/>.
|
||||
/// </summary>
|
||||
internal ResponseContinuationToken InnerToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the input messages used for streaming run.
|
||||
/// </summary>
|
||||
internal IEnumerable<ChatMessage>? InputMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response updates received so far.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<ChatResponseUpdate>? ResponseUpdates { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="ChatClientAgent"/> to enable discoverability of <see cref="ChatClientAgentRunOptions"/>.
|
||||
/// </summary>
|
||||
public partial class ChatClientAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
|
||||
/// </summary>
|
||||
/// <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 any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">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>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync(thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a text message from the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to send to the agent.</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 message and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">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>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
string message,
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a single chat message.
|
||||
/// </summary>
|
||||
/// <param name="message">The chat message to send to the agent.</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 message and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">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>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
ChatMessage message,
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent 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">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>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync(messages, thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode without providing new input messages, relying on existing context and instructions.
|
||||
/// </summary>
|
||||
/// <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 any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">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>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsync(thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode with a text message from the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to send to the agent.</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 message and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">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>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
string message,
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode with a single chat message.
|
||||
/// </summary>
|
||||
/// <param name="message">The chat message to send to the agent.</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 message and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">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>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
ChatMessage message,
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode 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">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>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread,
|
||||
ChatClientAgentRunOptions? options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsync(messages, thread, (AgentRunOptions?)options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread, and requesting a response of the specified type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <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 any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="useJsonSchemaResponseFormat">
|
||||
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
|
||||
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
|
||||
/// </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="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
|
||||
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
|
||||
AgentThread? thread,
|
||||
JsonSerializerOptions? serializerOptions,
|
||||
ChatClientAgentRunOptions? options,
|
||||
bool? useJsonSchemaResponseFormat = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync<T>(thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a text message from the user, requesting a response of the specified type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to send to the agent.</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 message and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="useJsonSchemaResponseFormat">
|
||||
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
|
||||
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
|
||||
/// </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="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
|
||||
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
|
||||
string message,
|
||||
AgentThread? thread,
|
||||
JsonSerializerOptions? serializerOptions,
|
||||
ChatClientAgentRunOptions? options,
|
||||
bool? useJsonSchemaResponseFormat = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync<T>(message, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a single chat message, requesting a response of the specified type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">The chat message to send to the agent.</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 message and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="useJsonSchemaResponseFormat">
|
||||
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
|
||||
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
|
||||
/// </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="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
|
||||
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
|
||||
ChatMessage message,
|
||||
AgentThread? thread,
|
||||
JsonSerializerOptions? serializerOptions,
|
||||
ChatClientAgentRunOptions? options,
|
||||
bool? useJsonSchemaResponseFormat = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync<T>(message, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a collection of chat messages, requesting a response of the specified type <typeparamref name="T"/>.
|
||||
/// </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="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="useJsonSchemaResponseFormat">
|
||||
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
|
||||
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
|
||||
/// </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="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
|
||||
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread,
|
||||
JsonSerializerOptions? serializerOptions,
|
||||
ChatClientAgentRunOptions? options,
|
||||
bool? useJsonSchemaResponseFormat = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync<T>(messages, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public sealed class ChatClientAgentOptions
|
||||
/// <summary>
|
||||
/// Context object passed to the <see cref="AIContextProviderFactory"/> to create a new instance of <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
public class AIContextProviderFactoryContext
|
||||
public sealed class AIContextProviderFactoryContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the serialized state of the <see cref="AIContextProvider"/>, if any.
|
||||
@@ -97,7 +97,7 @@ public sealed class ChatClientAgentOptions
|
||||
/// <summary>
|
||||
/// Context object passed to the <see cref="ChatMessageStoreFactory"/> to create a new instance of <see cref="ChatMessageStore"/>.
|
||||
/// </summary>
|
||||
public class ChatMessageStoreFactoryContext
|
||||
public sealed class ChatMessageStoreFactoryContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the serialized state of the chat message store, if any.
|
||||
|
||||
@@ -40,7 +40,6 @@ public sealed class ChatClientAgentRunResponse<T> : AgentRunResponse<T>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the response did not contain JSON, or if deserialization fails, this property will throw.
|
||||
/// To avoid exceptions, use <see cref="AgentRunResponse.TryDeserialize{T}"/> instead.
|
||||
/// </remarks>
|
||||
public override T Result => this._response.Result;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,10 @@ public sealed partial class ChatClientAgent
|
||||
|
||||
static ChatClientAgentRunResponse<T> CreateResponse(ChatResponse<T> chatResponse)
|
||||
{
|
||||
return new ChatClientAgentRunResponse<T>(chatResponse);
|
||||
return new ChatClientAgentRunResponse<T>(chatResponse)
|
||||
{
|
||||
ContinuationToken = WrapContinuationToken(chatResponse.ContinuationToken)
|
||||
};
|
||||
}
|
||||
|
||||
return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, thread, options, cancellationToken);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -832,6 +832,174 @@ public sealed class A2AAgentTests : IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithAgentMessageResponseMetadata_ReturnsMetadataAsAdditionalPropertiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.ResponseToReturn = new AgentMessage
|
||||
{
|
||||
MessageId = "response-123",
|
||||
Role = MessageRole.Agent,
|
||||
Parts = [new TextPart { Text = "Response with metadata" }],
|
||||
Metadata = new Dictionary<string, JsonElement>
|
||||
{
|
||||
{ "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
|
||||
{ "responseCount", JsonSerializer.SerializeToElement(99) }
|
||||
}
|
||||
};
|
||||
|
||||
var inputMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test message")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await this._agent.RunAsync(inputMessages);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.AdditionalProperties);
|
||||
Assert.NotNull(result.AdditionalProperties["responseKey1"]);
|
||||
Assert.Equal("responseValue1", ((JsonElement)result.AdditionalProperties["responseKey1"]!).GetString());
|
||||
Assert.NotNull(result.AdditionalProperties["responseCount"]);
|
||||
Assert.Equal(99, ((JsonElement)result.AdditionalProperties["responseCount"]!).GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.ResponseToReturn = new AgentMessage
|
||||
{
|
||||
MessageId = "response-123",
|
||||
Role = MessageRole.Agent,
|
||||
Parts = [new TextPart { Text = "Response" }]
|
||||
};
|
||||
|
||||
var inputMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test message")
|
||||
};
|
||||
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
AdditionalProperties = new()
|
||||
{
|
||||
{ "key1", "value1" },
|
||||
{ "key2", 42 },
|
||||
{ "key3", true }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await this._agent.RunAsync(inputMessages, null, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(this._handler.CapturedMessageSendParams);
|
||||
Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
|
||||
Assert.Equal("value1", this._handler.CapturedMessageSendParams.Metadata["key1"].GetString());
|
||||
Assert.Equal(42, this._handler.CapturedMessageSendParams.Metadata["key2"].GetInt32());
|
||||
Assert.True(this._handler.CapturedMessageSendParams.Metadata["key3"].GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.ResponseToReturn = new AgentMessage
|
||||
{
|
||||
MessageId = "response-123",
|
||||
Role = MessageRole.Agent,
|
||||
Parts = [new TextPart { Text = "Response" }]
|
||||
};
|
||||
|
||||
var inputMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test message")
|
||||
};
|
||||
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
AdditionalProperties = null
|
||||
};
|
||||
|
||||
// Act
|
||||
await this._agent.RunAsync(inputMessages, null, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(this._handler.CapturedMessageSendParams);
|
||||
Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.StreamingResponseToReturn = new AgentMessage
|
||||
{
|
||||
MessageId = "stream-123",
|
||||
Role = MessageRole.Agent,
|
||||
Parts = [new TextPart { Text = "Streaming response" }]
|
||||
};
|
||||
|
||||
var inputMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test streaming message")
|
||||
};
|
||||
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
AdditionalProperties = new()
|
||||
{
|
||||
{ "streamKey1", "streamValue1" },
|
||||
{ "streamKey2", 100 },
|
||||
{ "streamKey3", false }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options))
|
||||
{
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(this._handler.CapturedMessageSendParams);
|
||||
Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
|
||||
Assert.Equal("streamValue1", this._handler.CapturedMessageSendParams.Metadata["streamKey1"].GetString());
|
||||
Assert.Equal(100, this._handler.CapturedMessageSendParams.Metadata["streamKey2"].GetInt32());
|
||||
Assert.False(this._handler.CapturedMessageSendParams.Metadata["streamKey3"].GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.StreamingResponseToReturn = new AgentMessage
|
||||
{
|
||||
MessageId = "stream-123",
|
||||
Role = MessageRole.Agent,
|
||||
Parts = [new TextPart { Text = "Streaming response" }]
|
||||
};
|
||||
|
||||
var inputMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Test streaming message")
|
||||
};
|
||||
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
AdditionalProperties = null
|
||||
};
|
||||
|
||||
// Act
|
||||
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options))
|
||||
{
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(this._handler.CapturedMessageSendParams);
|
||||
Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithInvalidThreadType_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AdditionalPropertiesDictionaryExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class AdditionalPropertiesDictionaryExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithNullAdditionalProperties_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithEmptyAdditionalProperties_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = [];
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithStringValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "stringKey", "stringValue" }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("stringKey"));
|
||||
Assert.Equal("stringValue", result["stringKey"].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithNumericValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "numberKey", 42 }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("numberKey"));
|
||||
Assert.Equal(42, result["numberKey"].GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithBooleanValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "booleanKey", true }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("booleanKey"));
|
||||
Assert.True(result["booleanKey"].GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithMultipleProperties_ReturnsMetadataWithAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "stringKey", "stringValue" },
|
||||
{ "numberKey", 42 },
|
||||
{ "booleanKey", true }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(3, result.Count);
|
||||
|
||||
Assert.True(result.ContainsKey("stringKey"));
|
||||
Assert.Equal("stringValue", result["stringKey"].GetString());
|
||||
|
||||
Assert.True(result.ContainsKey("numberKey"));
|
||||
Assert.Equal(42, result["numberKey"].GetInt32());
|
||||
|
||||
Assert.True(result.ContainsKey("booleanKey"));
|
||||
Assert.True(result["booleanKey"].GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithArrayValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
int[] arrayValue = [1, 2, 3];
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "arrayKey", arrayValue }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("arrayKey"));
|
||||
Assert.Equal(JsonValueKind.Array, result["arrayKey"].ValueKind);
|
||||
Assert.Equal(3, result["arrayKey"].GetArrayLength());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithNullValue_ReturnsMetadataWithNullJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "nullKey", null! }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("nullKey"));
|
||||
Assert.Equal(JsonValueKind.Null, result["nullKey"].ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithJsonElementValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement jsonElement = JsonSerializer.SerializeToElement(new { name = "test", value = 123 });
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "jsonElementKey", jsonElement }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("jsonElementKey"));
|
||||
Assert.Equal(JsonValueKind.Object, result["jsonElementKey"].ValueKind);
|
||||
Assert.Equal("test", result["jsonElementKey"].GetProperty("name").GetString());
|
||||
Assert.Equal(123, result["jsonElementKey"].GetProperty("value").GetInt32());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -57,7 +57,7 @@ public class AgentRunResponseTests
|
||||
RawRepresentation = new object(),
|
||||
ResponseId = "responseId",
|
||||
Usage = new UsageDetails(),
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })
|
||||
};
|
||||
|
||||
AgentRunResponse response = new(chatResponse);
|
||||
@@ -214,6 +214,12 @@ public class AgentRunResponseTests
|
||||
Assert.Equal(100, usageContent.Details.TotalTokenCount);
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
/// <summary>
|
||||
/// Since Json Serialization using reflection is disabled in .net core builds, and we are using a custom type here that wouldn't
|
||||
/// be registered with the default source generated serializer, this test will only pass in .net framework builds where reflection-based
|
||||
/// serialization is available.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputSuccess()
|
||||
{
|
||||
@@ -221,6 +227,24 @@ public class AgentRunResponseTests
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
var animal = response.Deserialize<Animal>();
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(animal);
|
||||
Assert.Equal(expectedResult.Id, animal.Id);
|
||||
Assert.Equal(expectedResult.FullName, animal.FullName);
|
||||
Assert.Equal(expectedResult.Species, animal.Species);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputWithJSOSuccess()
|
||||
{
|
||||
// Arrange.
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
var animal = response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options);
|
||||
|
||||
@@ -262,6 +286,12 @@ public class AgentRunResponseTests
|
||||
Assert.Throws<JsonException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
/// <summary>
|
||||
/// Since Json Serialization using reflection is disabled in .net core builds, and we are using a custom type here that wouldn't
|
||||
/// be registered with the default source generated serializer, this test will only pass in .net framework builds where reflection-based
|
||||
/// serialization is available.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryParseAsStructuredOutputSuccess()
|
||||
{
|
||||
@@ -269,6 +299,24 @@ public class AgentRunResponseTests
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
response.TryDeserialize(out Animal? animal);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(animal);
|
||||
Assert.Equal(expectedResult.Id, animal.Id);
|
||||
Assert.Equal(expectedResult.FullName, animal.FullName);
|
||||
Assert.Equal(expectedResult.Species, animal.Species);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void TryParseAsStructuredOutputWithJSOSuccess()
|
||||
{
|
||||
// Arrange.
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
response.TryDeserialize(TestJsonSerializerContext.Default.Options, out Animal? animal);
|
||||
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AIAgentExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class AIAgentExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that when messageSendParams.Metadata is null, the options passed to RunAsync are null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WhenMetadataIsNull_PassesNullOptionsToRunAsync()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunOptions? capturedOptions = null;
|
||||
ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
|
||||
|
||||
// Act
|
||||
await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
|
||||
{
|
||||
Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
|
||||
Metadata = null
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Null(capturedOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when messageSendParams.Metadata has values, the options.AdditionalProperties contains the converted values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WhenMetadataHasValues_PassesOptionsWithAdditionalPropertiesToRunAsync()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunOptions? capturedOptions = null;
|
||||
ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
|
||||
|
||||
// Act
|
||||
await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
|
||||
{
|
||||
Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
|
||||
Metadata = new Dictionary<string, JsonElement>
|
||||
{
|
||||
["key1"] = JsonSerializer.SerializeToElement("value1"),
|
||||
["key2"] = JsonSerializer.SerializeToElement(42)
|
||||
}
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedOptions);
|
||||
Assert.NotNull(capturedOptions.AdditionalProperties);
|
||||
Assert.Equal(2, capturedOptions.AdditionalProperties.Count);
|
||||
Assert.True(capturedOptions.AdditionalProperties.ContainsKey("key1"));
|
||||
Assert.True(capturedOptions.AdditionalProperties.ContainsKey("key2"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when messageSendParams.Metadata is an empty dictionary, the options passed to RunAsync is null
|
||||
/// because the ToAdditionalProperties extension method returns null for empty dictionaries.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WhenMetadataIsEmptyDictionary_PassesNullOptionsToRunAsync()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunOptions? capturedOptions = null;
|
||||
ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
|
||||
|
||||
// Act
|
||||
await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
|
||||
{
|
||||
Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
|
||||
Metadata = []
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Null(capturedOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when the agent response has AdditionalProperties, the returned AgentMessage.Metadata contains the converted values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsAgentMessageWithMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProps = new()
|
||||
{
|
||||
["responseKey1"] = "responseValue1",
|
||||
["responseKey2"] = 123
|
||||
};
|
||||
AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
|
||||
{
|
||||
AdditionalProperties = additionalProps
|
||||
};
|
||||
ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
|
||||
|
||||
// Act
|
||||
A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
|
||||
{
|
||||
Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
|
||||
});
|
||||
|
||||
// Assert
|
||||
AgentMessage agentMessage = Assert.IsType<AgentMessage>(a2aResponse);
|
||||
Assert.NotNull(agentMessage.Metadata);
|
||||
Assert.Equal(2, agentMessage.Metadata.Count);
|
||||
Assert.True(agentMessage.Metadata.ContainsKey("responseKey1"));
|
||||
Assert.True(agentMessage.Metadata.ContainsKey("responseKey2"));
|
||||
Assert.Equal("responseValue1", agentMessage.Metadata["responseKey1"].GetString());
|
||||
Assert.Equal(123, agentMessage.Metadata["responseKey2"].GetInt32());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when the agent response has null AdditionalProperties, the returned AgentMessage.Metadata is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WhenResponseHasNullAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
|
||||
{
|
||||
AdditionalProperties = null
|
||||
};
|
||||
ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
|
||||
|
||||
// Act
|
||||
A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
|
||||
{
|
||||
Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
|
||||
});
|
||||
|
||||
// Assert
|
||||
AgentMessage agentMessage = Assert.IsType<AgentMessage>(a2aResponse);
|
||||
Assert.Null(agentMessage.Metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when the agent response has empty AdditionalProperties, the returned AgentMessage.Metadata is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WhenResponseHasEmptyAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
|
||||
{
|
||||
AdditionalProperties = []
|
||||
};
|
||||
ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
|
||||
|
||||
// Act
|
||||
A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
|
||||
{
|
||||
Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
|
||||
});
|
||||
|
||||
// Assert
|
||||
AgentMessage agentMessage = Assert.IsType<AgentMessage>(a2aResponse);
|
||||
Assert.Null(agentMessage.Metadata);
|
||||
}
|
||||
|
||||
private static Mock<AIAgent> CreateAgentMock(Action<AgentRunOptions?> optionsCallback)
|
||||
{
|
||||
Mock<AIAgent> agentMock = new() { CallBase = true };
|
||||
agentMock.SetupGet(x => x.Name).Returns("TestAgent");
|
||||
agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread());
|
||||
agentMock
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.Callback<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, CancellationToken>(
|
||||
(_, _, options, _) => optionsCallback(options))
|
||||
.ReturnsAsync(new AgentRunResponse([new ChatMessage(ChatRole.Assistant, "Test response")]));
|
||||
|
||||
return agentMock;
|
||||
}
|
||||
|
||||
private static Mock<AIAgent> CreateAgentMockWithResponse(AgentRunResponse response)
|
||||
{
|
||||
Mock<AIAgent> agentMock = new() { CallBase = true };
|
||||
agentMock.SetupGet(x => x.Name).Returns("TestAgent");
|
||||
agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread());
|
||||
agentMock
|
||||
.Protected()
|
||||
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentThread?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(response);
|
||||
|
||||
return agentMock;
|
||||
}
|
||||
|
||||
private static async Task<A2AResponse> InvokeOnMessageReceivedAsync(ITaskManager taskManager, MessageSendParams messageSendParams)
|
||||
{
|
||||
Func<MessageSendParams, CancellationToken, Task<A2AResponse>>? handler = taskManager.OnMessageReceived;
|
||||
Assert.NotNull(handler);
|
||||
return await handler.Invoke(messageSendParams, CancellationToken.None);
|
||||
}
|
||||
|
||||
private sealed class TestAgentThread : AgentThread;
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AdditionalPropertiesDictionaryExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class AdditionalPropertiesDictionaryExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithNullAdditionalProperties_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithEmptyAdditionalProperties_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = [];
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithStringValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "stringKey", "stringValue" }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("stringKey"));
|
||||
Assert.Equal("stringValue", result["stringKey"].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithNumericValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "numberKey", 42 }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("numberKey"));
|
||||
Assert.Equal(42, result["numberKey"].GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithBooleanValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "booleanKey", true }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("booleanKey"));
|
||||
Assert.True(result["booleanKey"].GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithMultipleProperties_ReturnsMetadataWithAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "stringKey", "stringValue" },
|
||||
{ "numberKey", 42 },
|
||||
{ "booleanKey", true }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(3, result.Count);
|
||||
|
||||
Assert.True(result.ContainsKey("stringKey"));
|
||||
Assert.Equal("stringValue", result["stringKey"].GetString());
|
||||
|
||||
Assert.True(result.ContainsKey("numberKey"));
|
||||
Assert.Equal(42, result["numberKey"].GetInt32());
|
||||
|
||||
Assert.True(result.ContainsKey("booleanKey"));
|
||||
Assert.True(result["booleanKey"].GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithArrayValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
int[] arrayValue = [1, 2, 3];
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "arrayKey", arrayValue }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("arrayKey"));
|
||||
Assert.Equal(JsonValueKind.Array, result["arrayKey"].ValueKind);
|
||||
Assert.Equal(3, result["arrayKey"].GetArrayLength());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithNullValue_ReturnsMetadataWithNullJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "nullKey", null! }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("nullKey"));
|
||||
Assert.Equal(JsonValueKind.Null, result["nullKey"].ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMetadata_WithJsonElementValue_ReturnsMetadataWithJsonElement()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement jsonElement = JsonSerializer.SerializeToElement(new { name = "test", value = 123 });
|
||||
AdditionalPropertiesDictionary additionalProperties = new()
|
||||
{
|
||||
{ "jsonElementKey", jsonElement }
|
||||
};
|
||||
|
||||
// Act
|
||||
Dictionary<string, JsonElement>? result = additionalProperties.ToA2AMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.True(result.ContainsKey("jsonElementKey"));
|
||||
Assert.Equal(JsonValueKind.Object, result["jsonElementKey"].ValueKind);
|
||||
Assert.Equal("test", result["jsonElementKey"].GetProperty("name").GetString());
|
||||
Assert.Equal(123, result["jsonElementKey"].GetProperty("value").GetInt32());
|
||||
}
|
||||
}
|
||||
+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
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.ChatClient;
|
||||
|
||||
public class ChatClientAgentContinuationTokenTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToBytes_Roundtrip()
|
||||
{
|
||||
// Arrange
|
||||
ResponseContinuationToken originalToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3, 4, 5 });
|
||||
|
||||
ChatClientAgentContinuationToken chatClientToken = new(originalToken)
|
||||
{
|
||||
InputMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello!"),
|
||||
new ChatMessage(ChatRole.User, "How are you?")
|
||||
],
|
||||
ResponseUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "I'm fine, thank you."),
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "How can I assist you today?")
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
ReadOnlyMemory<byte> bytes = chatClientToken.ToBytes();
|
||||
|
||||
ChatClientAgentContinuationToken tokenFromBytes = ChatClientAgentContinuationToken.FromToken(ResponseContinuationToken.FromBytes(bytes));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(tokenFromBytes);
|
||||
Assert.Equal(chatClientToken.ToBytes().ToArray(), tokenFromBytes.ToBytes().ToArray());
|
||||
|
||||
// Verify InnerToken
|
||||
Assert.Equal(chatClientToken.InnerToken.ToBytes().ToArray(), tokenFromBytes.InnerToken.ToBytes().ToArray());
|
||||
|
||||
// Verify InputMessages
|
||||
Assert.NotNull(tokenFromBytes.InputMessages);
|
||||
Assert.Equal(chatClientToken.InputMessages.Count(), tokenFromBytes.InputMessages.Count());
|
||||
for (int i = 0; i < chatClientToken.InputMessages.Count(); i++)
|
||||
{
|
||||
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Role, tokenFromBytes.InputMessages.ElementAt(i).Role);
|
||||
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Text, tokenFromBytes.InputMessages.ElementAt(i).Text);
|
||||
}
|
||||
|
||||
// Verify ResponseUpdates
|
||||
Assert.NotNull(tokenFromBytes.ResponseUpdates);
|
||||
Assert.Equal(chatClientToken.ResponseUpdates.Count, tokenFromBytes.ResponseUpdates.Count);
|
||||
for (int i = 0; i < chatClientToken.ResponseUpdates.Count; i++)
|
||||
{
|
||||
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Role, tokenFromBytes.ResponseUpdates.ElementAt(i).Role);
|
||||
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Text, tokenFromBytes.ResponseUpdates.ElementAt(i).Text);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialization_Roundtrip()
|
||||
{
|
||||
// Arrange
|
||||
ResponseContinuationToken originalToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3, 4, 5 });
|
||||
|
||||
ChatClientAgentContinuationToken chatClientToken = new(originalToken)
|
||||
{
|
||||
InputMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello!"),
|
||||
new ChatMessage(ChatRole.User, "How are you?")
|
||||
],
|
||||
ResponseUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "I'm fine, thank you."),
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "How can I assist you today?")
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(chatClientToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
|
||||
|
||||
ResponseContinuationToken? deserializedToken = (ResponseContinuationToken?)JsonSerializer.Deserialize(json, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
|
||||
|
||||
ChatClientAgentContinuationToken deserializedChatClientToken = ChatClientAgentContinuationToken.FromToken(deserializedToken!);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserializedChatClientToken);
|
||||
Assert.Equal(chatClientToken.ToBytes().ToArray(), deserializedChatClientToken.ToBytes().ToArray());
|
||||
|
||||
// Verify InnerToken
|
||||
Assert.Equal(chatClientToken.InnerToken.ToBytes().ToArray(), deserializedChatClientToken.InnerToken.ToBytes().ToArray());
|
||||
|
||||
// Verify InputMessages
|
||||
Assert.NotNull(deserializedChatClientToken.InputMessages);
|
||||
Assert.Equal(chatClientToken.InputMessages.Count(), deserializedChatClientToken.InputMessages.Count());
|
||||
for (int i = 0; i < chatClientToken.InputMessages.Count(); i++)
|
||||
{
|
||||
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Role, deserializedChatClientToken.InputMessages.ElementAt(i).Role);
|
||||
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Text, deserializedChatClientToken.InputMessages.ElementAt(i).Text);
|
||||
}
|
||||
|
||||
// Verify ResponseUpdates
|
||||
Assert.NotNull(deserializedChatClientToken.ResponseUpdates);
|
||||
Assert.Equal(chatClientToken.ResponseUpdates.Count, deserializedChatClientToken.ResponseUpdates.Count);
|
||||
for (int i = 0; i < chatClientToken.ResponseUpdates.Count; i++)
|
||||
{
|
||||
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Role, deserializedChatClientToken.ResponseUpdates.ElementAt(i).Role);
|
||||
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Text, deserializedChatClientToken.ResponseUpdates.ElementAt(i).Text);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromToken_WithChatClientAgentContinuationToken_ReturnsSameInstance()
|
||||
{
|
||||
// Arrange
|
||||
ChatClientAgentContinuationToken originalToken = new(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3, 4, 5 }));
|
||||
|
||||
// Act
|
||||
ChatClientAgentContinuationToken fromToken = ChatClientAgentContinuationToken.FromToken(originalToken);
|
||||
|
||||
// Assert
|
||||
Assert.Same(originalToken, fromToken);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+278
-113
@@ -18,10 +18,10 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task RunAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
|
||||
public async Task RunAsync_PropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
|
||||
{
|
||||
// Arrange
|
||||
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
var continuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }));
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
@@ -63,15 +63,15 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.True(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken, capturedChatOptions.ContinuationToken);
|
||||
Assert.Same(continuationToken.InnerToken, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
|
||||
public async Task RunAsync_WhenPropertiesSetInBothLocations_PrioritizesAgentRunOptionsOverChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var continuationToken1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
var continuationToken2 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
var continuationToken1 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }));
|
||||
var continuationToken2 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }));
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
@@ -104,13 +104,13 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.False(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken);
|
||||
Assert.Same(continuationToken2.InnerToken, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task RunStreamingAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
|
||||
public async Task RunStreamingAsync_PropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
@@ -119,7 +119,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "at?") { ConversationId = "conversation-id" },
|
||||
];
|
||||
|
||||
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
var continuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] };
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
@@ -164,11 +164,11 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
|
||||
Assert.True(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken, capturedChatOptions.ContinuationToken);
|
||||
Assert.Same(continuationToken.InnerToken, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
|
||||
public async Task RunStreamingAsync_WhenPropertiesSetInBothLocations_PrioritizesAgentRunOptionsOverChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
@@ -176,8 +176,8 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh") { ConversationId = "conversation-id" },
|
||||
];
|
||||
|
||||
var continuationToken1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
var continuationToken2 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
var continuationToken1 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] };
|
||||
var continuationToken2 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] };
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
@@ -212,11 +212,11 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.False(capturedChatOptions.AllowBackgroundResponses);
|
||||
Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken);
|
||||
Assert.Same(continuationToken2.InnerToken, capturedChatOptions.ContinuationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncPropagatesContinuationTokenFromChatResponseToAgentRunResponseAsync()
|
||||
public async Task RunAsync_WhenContinuationTokenReceivedFromChatResponse_WrapsContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
@@ -237,11 +237,11 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
var response = await agent.RunAsync([new(ChatRole.User, "hi")], thread, options: runOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Same(continuationToken, response.ContinuationToken);
|
||||
Assert.Same(continuationToken, (response.ContinuationToken as ChatClientAgentContinuationToken)?.InnerToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync()
|
||||
public async Task RunStreamingAsync_WhenContinuationTokenReceived_WrapsContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
var token1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
|
||||
@@ -272,19 +272,19 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, actualUpdates.Count);
|
||||
Assert.Same(token1, actualUpdates[0].ContinuationToken);
|
||||
Assert.Same(token1, (actualUpdates[0].ContinuationToken as ChatClientAgentContinuationToken)?.InnerToken);
|
||||
Assert.Null(actualUpdates[1].ContinuationToken); // last update has null token
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync()
|
||||
public async Task RunAsync_WhenMessagesProvidedWithContinuationToken_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) };
|
||||
|
||||
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
|
||||
|
||||
@@ -301,14 +301,14 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync()
|
||||
public async Task RunStreamingAsync_WhenMessagesProvidedWithContinuationToken_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) };
|
||||
|
||||
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
|
||||
|
||||
@@ -331,7 +331,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync()
|
||||
public async Task RunAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopulationAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> capturedMessages = [];
|
||||
@@ -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
|
||||
@@ -371,7 +371,10 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
AIContextProvider = mockContextProvider.Object
|
||||
};
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
|
||||
};
|
||||
|
||||
// Act
|
||||
await agent.RunAsync([], thread, options: runOptions);
|
||||
@@ -383,7 +386,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
|
||||
@@ -393,7 +396,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync()
|
||||
public async Task RunStreamingAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopulationAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> capturedMessages = [];
|
||||
@@ -401,7 +404,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
|
||||
@@ -433,20 +436,21 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
AIContextProvider = mockContextProvider.Object
|
||||
};
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] }
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<NotSupportedException>(async () => await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync());
|
||||
await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Streaming resumption is only supported when chat history is stored and managed by the underlying AI service.", exception.Message);
|
||||
|
||||
// With continuation token, thread message population should be skipped
|
||||
Assert.Empty(capturedMessages);
|
||||
|
||||
// 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
|
||||
@@ -456,7 +460,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync()
|
||||
public async Task RunAsync_WhenNoThreadProvidedForBackgroundResponses_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
@@ -480,7 +484,7 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync()
|
||||
public async Task RunStreamingAsync_WhenNoThreadProvidedForBackgroundResponses_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
@@ -510,126 +514,287 @@ public class ChatClientAgent_BackgroundResponsesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncThrowsWhenContinuationTokenProvidedForInitialRunAsync()
|
||||
public async Task RunStreamingAsync_WhenInputMessagesPresentInContinuationToken_ResumesStreamingAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "continuation") { ConversationId = "conversation-id" },
|
||||
];
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Create a new thread with no ConversationId and no MessageStore (initial run state)
|
||||
ChatClientAgentThread thread = new();
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(thread: thread, options: runOptions));
|
||||
Assert.Equal("Continuation tokens are not allowed to be used for initial runs.", exception.Message);
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetResponseAsync(
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenContinuationTokenProvidedForInitialRunAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(returnUpdates));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Create a new thread with no ConversationId and no MessageStore (initial run state)
|
||||
ChatClientAgentThread thread = new();
|
||||
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await agent.RunStreamingAsync(thread: thread, options: runOptions).ToListAsync());
|
||||
Assert.Equal("Continuation tokens are not allowed to be used for initial runs.", exception.Message);
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenContinuationTokenUsedWithClientSideManagedChatHistoryAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Create a thread with a MessageStore
|
||||
ChatClientAgentThread thread = new()
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
MessageStore = new InMemoryChatMessageStore(), // Setting a message store to skip checking the continuation token in the initial run
|
||||
ConversationId = null, // No conversation ID to simulate client-side managed chat history
|
||||
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
|
||||
{
|
||||
InputMessages = [new ChatMessage(ChatRole.User, "previous message")]
|
||||
}
|
||||
};
|
||||
|
||||
// Create run options with a continuation token
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
// Act
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync(thread, options: runOptions))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotSupportedException>(async () => await agent.RunStreamingAsync(thread: thread, options: runOptions).ToListAsync());
|
||||
Assert.Equal("Streaming resumption is only supported when chat history is stored and managed by the underlying AI service.", exception.Message);
|
||||
// Assert
|
||||
Assert.Single(updates);
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
// Verify that the IChatClient was called
|
||||
mockChatClient.Verify(
|
||||
c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsyncThrowsWhenContinuationTokenUsedWithAIContextProviderAsync()
|
||||
public async Task RunStreamingAsync_WhenResponseUpdatesPresentInContinuationToken_ResumesStreamingAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "continuation") { ConversationId = "conversation-id" },
|
||||
];
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(returnUpdates));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
// Create a mock AIContextProvider
|
||||
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
|
||||
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
|
||||
{
|
||||
ResponseUpdates = [new ChatResponseUpdate(ChatRole.Assistant, "previous update")]
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync(thread, options: runOptions))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Single(updates);
|
||||
|
||||
// Verify that the IChatClient was called
|
||||
mockChatClient.Verify(
|
||||
c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitialRunForContextProviderAndMessageStoreAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatResponseUpdate[] returnUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "upon"),
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: " a"),
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: " time"),
|
||||
];
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(returnUpdates));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
List<ChatMessage> capturedMessagesAddedToStore = [];
|
||||
var mockMessageStore = new Mock<ChatMessageStore>();
|
||||
mockMessageStore
|
||||
.Setup(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ChatMessageStore.InvokedContext, CancellationToken>((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.ResponseMessages ?? []))
|
||||
.Returns(new ValueTask());
|
||||
|
||||
AIContextProvider.InvokedContext? capturedInvokedContext = null;
|
||||
var mockContextProvider = new Mock<AIContextProvider>();
|
||||
mockContextProvider
|
||||
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AIContext());
|
||||
mockContextProvider
|
||||
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Setup(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<AIContextProvider.InvokedContext, CancellationToken>((context, ct) => capturedInvokedContext = context)
|
||||
.Returns(new ValueTask());
|
||||
|
||||
// Create a thread with an AIContextProvider and conversation ID to simulate non-initial run
|
||||
ChatClientAgentThread thread = new()
|
||||
{
|
||||
ConversationId = "existing-conversation-id",
|
||||
MessageStore = mockMessageStore.Object,
|
||||
AIContextProvider = mockContextProvider.Object
|
||||
};
|
||||
|
||||
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
|
||||
{
|
||||
ResponseUpdates = [new ChatResponseUpdate(ChatRole.Assistant, "once ")]
|
||||
}
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotSupportedException>(async () => await agent.RunStreamingAsync(thread: thread, options: runOptions).ToListAsync());
|
||||
// Act
|
||||
await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync();
|
||||
|
||||
Assert.Equal("Using context provider with streaming resumption is not supported.", exception.Message);
|
||||
// Assert
|
||||
mockMessageStore.Verify(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
Assert.Single(capturedMessagesAddedToStore);
|
||||
Assert.Contains("once upon a time", capturedMessagesAddedToStore[0].Text);
|
||||
|
||||
// Verify that the IChatClient was never called due to early validation
|
||||
mockChatClient.Verify(
|
||||
c => c.GetStreamingResponseAsync(
|
||||
mockContextProvider.Verify(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
Assert.NotNull(capturedInvokedContext?.ResponseMessages);
|
||||
Assert.Single(capturedInvokedContext.ResponseMessages);
|
||||
Assert.Contains("once upon a time", capturedInvokedContext.ResponseMessages.ElementAt(0).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromInitialRunForContextProviderAndMessageStoreAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(Array.Empty<ChatResponseUpdate>()));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
List<ChatMessage> capturedMessagesAddedToStore = [];
|
||||
var mockMessageStore = new Mock<ChatMessageStore>();
|
||||
mockMessageStore
|
||||
.Setup(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ChatMessageStore.InvokedContext, CancellationToken>((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.RequestMessages))
|
||||
.Returns(new ValueTask());
|
||||
|
||||
AIContextProvider.InvokedContext? capturedInvokedContext = null;
|
||||
var mockContextProvider = new Mock<AIContextProvider>();
|
||||
mockContextProvider
|
||||
.Setup(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<AIContextProvider.InvokedContext, CancellationToken>((context, ct) => capturedInvokedContext = context)
|
||||
.Returns(new ValueTask());
|
||||
|
||||
ChatClientAgentThread thread = new()
|
||||
{
|
||||
MessageStore = mockMessageStore.Object,
|
||||
AIContextProvider = mockContextProvider.Object
|
||||
};
|
||||
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
|
||||
{
|
||||
InputMessages = [new ChatMessage(ChatRole.User, "Tell me a story")],
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync();
|
||||
|
||||
// Assert
|
||||
mockMessageStore.Verify(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
Assert.Single(capturedMessagesAddedToStore);
|
||||
Assert.Contains("Tell me a story", capturedMessagesAddedToStore[0].Text);
|
||||
|
||||
mockContextProvider.Verify(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
Assert.NotNull(capturedInvokedContext?.RequestMessages);
|
||||
Assert.Single(capturedInvokedContext.RequestMessages);
|
||||
Assert.Contains("Tell me a story", capturedInvokedContext.RequestMessages.ElementAt(0).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WhenResumingStreaming_SavesInputMessagesAndUpdatesInContinuationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> returnUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: "Once") { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: " upon") { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: " a") { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
|
||||
new ChatResponseUpdate(role: ChatRole.Assistant, content: " time"){ ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
|
||||
];
|
||||
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient
|
||||
.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(returnUpdates));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
|
||||
ChatClientAgentThread thread = new() { };
|
||||
|
||||
List<ChatClientAgentContinuationToken> capturedContinuationTokens = [];
|
||||
|
||||
ChatMessage userMessage = new(ChatRole.User, "Tell me a story");
|
||||
|
||||
// Act
|
||||
|
||||
// Do the initial run
|
||||
await foreach (var update in agent.RunStreamingAsync(userMessage, thread))
|
||||
{
|
||||
capturedContinuationTokens.Add(Assert.IsType<ChatClientAgentContinuationToken>(update.ContinuationToken));
|
||||
break;
|
||||
}
|
||||
|
||||
// Now resume the run using the captured continuation token
|
||||
returnUpdates.RemoveAt(0); // remove the first mock update as it was already processed
|
||||
var options = new AgentRunOptions { ContinuationToken = capturedContinuationTokens[0] };
|
||||
await foreach (var update in agent.RunStreamingAsync(thread, options: options))
|
||||
{
|
||||
capturedContinuationTokens.Add(Assert.IsType<ChatClientAgentContinuationToken>(update.ContinuationToken));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, capturedContinuationTokens.Count);
|
||||
|
||||
// Verify that the first continuation token has the initial input and first update
|
||||
Assert.NotNull(capturedContinuationTokens[0].InputMessages);
|
||||
Assert.Single(capturedContinuationTokens[0].InputMessages!);
|
||||
Assert.Equal("Tell me a story", capturedContinuationTokens[0].InputMessages!.Last().Text);
|
||||
Assert.NotNull(capturedContinuationTokens[0].ResponseUpdates);
|
||||
Assert.Single(capturedContinuationTokens[0].ResponseUpdates!);
|
||||
Assert.Equal("Once", capturedContinuationTokens[0].ResponseUpdates![0].Text);
|
||||
|
||||
// Verify the last continuation token has the input and all updates
|
||||
var lastToken = capturedContinuationTokens[^1];
|
||||
Assert.NotNull(lastToken.InputMessages);
|
||||
Assert.Single(lastToken.InputMessages!);
|
||||
Assert.Equal("Tell me a story", lastToken.InputMessages!.Last().Text);
|
||||
Assert.NotNull(lastToken.ResponseUpdates);
|
||||
Assert.Equal(4, lastToken.ResponseUpdates!.Count);
|
||||
Assert.Equal("Once", lastToken.ResponseUpdates!.ElementAt(0).Text);
|
||||
Assert.Equal(" upon", lastToken.ResponseUpdates!.ElementAt(1).Text);
|
||||
Assert.Equal(" a", lastToken.ResponseUpdates!.ElementAt(2).Text);
|
||||
Assert.Equal(" time", lastToken.ResponseUpdates!.ElementAt(3).Text);
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
|
||||
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ChatClientAgent"/> run methods with <see cref="ChatClientAgentRunOptions"/>.
|
||||
/// </summary>
|
||||
public sealed partial class ChatClientAgent_RunWithCustomOptionsTests
|
||||
{
|
||||
#region RunAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithThreadAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse result = await agent.RunAsync(thread, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Messages);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithStringMessageAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse result = await agent.RunAsync("Test message", thread, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Messages);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithChatMessageAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatMessage message = new(ChatRole.User, "Test message");
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse result = await agent.RunAsync(message, thread, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Messages);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")];
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse result = await agent.RunAsync(messages, thread, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result.Messages);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WithChatOptionsInRunOptions_UsesChatOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
ChatClientAgentRunOptions options = new(new ChatOptions { Temperature = 0.5f });
|
||||
|
||||
// Act
|
||||
AgentRunResponse result = await agent.RunAsync("Test", null, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.Is<ChatOptions>(opts => opts.Temperature == 0.5f),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RunStreamingAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WithThreadAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync(thread, options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(updates);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WithStringMessageAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync("Test message", thread, options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(updates);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetStreamingResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WithChatMessageAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatMessage message = new(ChatRole.User, "Test message");
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync(message, thread, options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(updates);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetStreamingResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStreamingAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
IEnumerable<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Message 1"), new ChatMessage(ChatRole.User, "Message 2")];
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync(messages, thread, options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(updates);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static async IAsyncEnumerable<ChatResponseUpdate> GetAsyncUpdatesAsync()
|
||||
{
|
||||
yield return new ChatResponseUpdate { Contents = new[] { new TextContent("Hello") } };
|
||||
yield return new ChatResponseUpdate { Contents = new[] { new TextContent(" World") } };
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RunAsync{T} Tests
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncOfT_WithThreadAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>(thread, JsonContext_WithCustomRunOptions.Default.Options, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agentRunResponse);
|
||||
Assert.Single(agentRunResponse.Messages);
|
||||
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncOfT_WithStringMessageAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>("Test message", thread, JsonContext_WithCustomRunOptions.Default.Options, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agentRunResponse);
|
||||
Assert.Single(agentRunResponse.Messages);
|
||||
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncOfT_WithChatMessageAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
ChatMessage message = new(ChatRole.User, "Test message");
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>(message, thread, JsonContext_WithCustomRunOptions.Default.Options, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agentRunResponse);
|
||||
Assert.Single(agentRunResponse.Messages);
|
||||
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsyncOfT_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockChatClient = new();
|
||||
mockChatClient.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
|
||||
|
||||
ChatClientAgent agent = new(mockChatClient.Object);
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")];
|
||||
ChatClientAgentRunOptions options = new();
|
||||
|
||||
// Act
|
||||
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>(messages, thread, JsonContext_WithCustomRunOptions.Default.Options, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agentRunResponse);
|
||||
Assert.Single(agentRunResponse.Messages);
|
||||
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
|
||||
mockChatClient.Verify(
|
||||
x => x.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class Animal
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? FullName { get; set; }
|
||||
public Species Species { get; set; }
|
||||
}
|
||||
|
||||
private enum Species
|
||||
{
|
||||
Bear,
|
||||
Tiger,
|
||||
Walrus,
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
[JsonSerializable(typeof(Animal))]
|
||||
private sealed partial class JsonContext_WithCustomRunOptions : JsonSerializerContext;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user