mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into dev/dotnet_workflow/fix_handoff_agent_session_handling
This commit is contained in:
@@ -152,6 +152,7 @@
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/Evaluation/">
|
||||
<Project Path="samples/02-agents/Evaluation/Evaluation_SimpleEval/Evaluation_SimpleEval.csproj" />
|
||||
@@ -173,6 +174,7 @@
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentWithRAG/">
|
||||
<File Path="samples/02-agents/AgentWithRAG/README.md" />
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to download files generated by Code Interpreter using the Containers API.
|
||||
// Code Interpreter generates files inside containers (cfile_ / cntr_ IDs) which cannot be
|
||||
// downloaded via the standard Files API. Use ContainerClient instead.
|
||||
|
||||
#pragma warning disable OPENAI001
|
||||
|
||||
using System.ClientModel;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Containers;
|
||||
using OpenAI.Responses;
|
||||
|
||||
string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
||||
string model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
var openAIClient = new OpenAIClient(new ApiKeyCredential(apiKey));
|
||||
|
||||
// Create an agent with Code Interpreter tool enabled
|
||||
AIAgent agent = openAIClient
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(
|
||||
model: model,
|
||||
instructions: "You are a helpful assistant that can generate files using code.",
|
||||
name: "CodeInterpreterAgent",
|
||||
tools: [new HostedCodeInterpreterTool()]);
|
||||
|
||||
// Ask the agent to generate a file
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"Create a CSV file with the multiplication times tables from 1 to 12. Include headers.");
|
||||
|
||||
// Display the text response
|
||||
foreach (TextContent textContent in response.Messages.SelectMany(x => x.Contents).OfType<TextContent>())
|
||||
{
|
||||
Console.WriteLine(textContent.Text);
|
||||
}
|
||||
|
||||
// Extract container file citations from response annotations and download
|
||||
ContainerClient containerClient = openAIClient.GetContainerClient();
|
||||
|
||||
HashSet<string> downloadedFiles = [];
|
||||
bool foundContainerFiles = false;
|
||||
|
||||
foreach (AIContent content in response.Messages.SelectMany(x => x.Contents))
|
||||
{
|
||||
if (content.Annotations is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (AIAnnotation annotation in content.Annotations)
|
||||
{
|
||||
// Container files from Code Interpreter have ContainerFileCitationMessageAnnotation as raw representation
|
||||
if (annotation is CitationAnnotation citation
|
||||
&& citation.RawRepresentation is ContainerFileCitationMessageAnnotation containerCitation)
|
||||
{
|
||||
foundContainerFiles = true;
|
||||
|
||||
// Deduplicate by container+file ID in case the same file is cited multiple times
|
||||
string key = $"{containerCitation.ContainerId}/{containerCitation.FileId}";
|
||||
if (!downloadedFiles.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nDownloading container file: {containerCitation.Filename}");
|
||||
Console.WriteLine($" Container ID: {containerCitation.ContainerId}");
|
||||
Console.WriteLine($" File ID: {containerCitation.FileId}");
|
||||
|
||||
BinaryData fileData = await containerClient.DownloadContainerFileAsync(
|
||||
containerCitation.ContainerId,
|
||||
containerCitation.FileId);
|
||||
|
||||
// Sanitize filename to prevent path traversal
|
||||
string safeFilename = Path.GetFileName(containerCitation.Filename);
|
||||
string outputPath = Path.Combine(Directory.GetCurrentDirectory(), safeFilename);
|
||||
await File.WriteAllBytesAsync(outputPath, fileData.ToArray());
|
||||
Console.WriteLine($" Saved to: {outputPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundContainerFiles)
|
||||
{
|
||||
Console.WriteLine("\nNo container file citations found in the response.");
|
||||
Console.WriteLine("The model may not have generated a downloadable file for this prompt.");
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Code Interpreter File Download (OpenAI)
|
||||
|
||||
This sample demonstrates how to download files generated by Code Interpreter when using the OpenAI Responses API.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating an agent with Code Interpreter tool using `ResponsesClient.AsAIAgent()`
|
||||
- Generating files through Code Interpreter (e.g., CSV, Excel, images)
|
||||
- Extracting container file citations from agent response annotations
|
||||
- Downloading container files using the `ContainerClient` API
|
||||
|
||||
## Container files vs regular files
|
||||
|
||||
When Code Interpreter generates a file, the file is stored inside a **container** with a `cntr_` prefixed ID. The file itself gets a `cfile_` prefixed ID.
|
||||
|
||||
These container files **cannot** be downloaded using the standard Files API (`GetOpenAIFileClient`), which returns 404 for `cfile_` IDs. Instead, you must use the **Containers API** (`GetContainerClient`) to download them:
|
||||
|
||||
```csharp
|
||||
// ❌ This does NOT work for container files
|
||||
var filesClient = openAIClient.GetOpenAIFileClient();
|
||||
await filesClient.DownloadFileAsync("cfile_..."); // Returns 404
|
||||
|
||||
// ✅ Use ContainerClient instead
|
||||
var containerClient = openAIClient.GetContainerClient();
|
||||
await containerClient.DownloadContainerFileAsync("cntr_...", "cfile_...");
|
||||
```
|
||||
|
||||
The container ID and file ID are available from the `ContainerFileCitationMessageAnnotation` annotation in the response, accessible via `CitationAnnotation.RawRepresentation`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- OpenAI API key with access to a model that supports Code Interpreter
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:OPENAI_API_KEY="sk-..."
|
||||
$env:OPENAI_CHAT_MODEL_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Code Interpreter File Download with Foundry](../../../02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/) — same scenario using Microsoft Foundry
|
||||
- [Code Interpreter](../../../02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/) — Code Interpreter without file download
|
||||
@@ -14,4 +14,5 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|
||||
|[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.|
|
||||
|[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.|
|
||||
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|
||||
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentSession for context continuity.|
|
||||
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentSession for context continuity.|
|
||||
|[Code Interpreter File Download](./Agent_OpenAI_Step06_CodeInterpreterFileDownload/)|This sample demonstrates how to download files generated by Code Interpreter using the Containers API (`cfile_`/`cntr_` IDs).|
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to download files generated by Code Interpreter using Microsoft Foundry.
|
||||
// Code Interpreter generates files inside containers (cfile_ / cntr_ IDs) which cannot be
|
||||
// downloaded via the standard Files API. Use ContainerClient from the project's OpenAI client instead.
|
||||
|
||||
#pragma warning disable OPENAI001
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Create an agent with Code Interpreter tool enabled
|
||||
AIAgent agent = aiProjectClient.AsAIAgent(
|
||||
deploymentName,
|
||||
instructions: "You are a helpful assistant that can generate files using code.",
|
||||
name: "CodeInterpreterAgent",
|
||||
tools: [new HostedCodeInterpreterTool()]);
|
||||
|
||||
// Ask the agent to generate a file
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"Create a CSV file with the multiplication times tables from 1 to 12. Include headers.");
|
||||
|
||||
// Display the text response
|
||||
foreach (TextContent textContent in response.Messages.SelectMany(x => x.Contents).OfType<TextContent>())
|
||||
{
|
||||
Console.WriteLine(textContent.Text);
|
||||
}
|
||||
|
||||
// Extract container file citations from response annotations and download.
|
||||
// AIProjectClient.GetProjectOpenAIClient() returns a ProjectOpenAIClient (inherits from OpenAI.OpenAIClient)
|
||||
// which supports GetContainerClient(), unlike AzureOpenAIClient which does not.
|
||||
var containerClient = aiProjectClient.GetProjectOpenAIClient().GetContainerClient();
|
||||
|
||||
HashSet<string> downloadedFiles = [];
|
||||
bool foundContainerFiles = false;
|
||||
|
||||
foreach (AIContent content in response.Messages.SelectMany(x => x.Contents))
|
||||
{
|
||||
if (content.Annotations is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (AIAnnotation annotation in content.Annotations)
|
||||
{
|
||||
// Container files from Code Interpreter have ContainerFileCitationMessageAnnotation as raw representation
|
||||
if (annotation is CitationAnnotation citation
|
||||
&& citation.RawRepresentation is ContainerFileCitationMessageAnnotation containerCitation)
|
||||
{
|
||||
foundContainerFiles = true;
|
||||
|
||||
// Deduplicate by container+file ID in case the same file is cited multiple times
|
||||
string key = $"{containerCitation.ContainerId}/{containerCitation.FileId}";
|
||||
if (!downloadedFiles.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nDownloading container file: {containerCitation.Filename}");
|
||||
Console.WriteLine($" Container ID: {containerCitation.ContainerId}");
|
||||
Console.WriteLine($" File ID: {containerCitation.FileId}");
|
||||
|
||||
BinaryData fileData = await containerClient.DownloadContainerFileAsync(
|
||||
containerCitation.ContainerId,
|
||||
containerCitation.FileId);
|
||||
|
||||
// Sanitize filename to prevent path traversal
|
||||
string safeFilename = Path.GetFileName(containerCitation.Filename);
|
||||
string outputPath = Path.Combine(Directory.GetCurrentDirectory(), safeFilename);
|
||||
await File.WriteAllBytesAsync(outputPath, fileData.ToArray());
|
||||
Console.WriteLine($" Saved to: {outputPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundContainerFiles)
|
||||
{
|
||||
Console.WriteLine("\nNo container file citations found in the response.");
|
||||
Console.WriteLine("The model may not have generated a downloadable file for this prompt.");
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# Code Interpreter File Download (Microsoft Foundry)
|
||||
|
||||
This sample demonstrates how to download files generated by Code Interpreter when using Microsoft Foundry.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating an agent with Code Interpreter tool using `AIProjectClient.AsAIAgent()`
|
||||
- Generating files through Code Interpreter (e.g., CSV, Excel, images)
|
||||
- Extracting container file citations from agent response annotations
|
||||
- Downloading container files using the `ContainerClient` via `AIProjectClient.GetProjectOpenAIClient()`
|
||||
|
||||
## Container files vs regular files
|
||||
|
||||
When Code Interpreter generates a file, the file is stored inside a **container** with a `cntr_` prefixed ID. The file itself gets a `cfile_` prefixed ID.
|
||||
|
||||
These container files **cannot** be downloaded using the standard Files API (`GetOpenAIFileClient`), which returns 404 for `cfile_` IDs. Instead, you must use the **Containers API** to download them.
|
||||
|
||||
### Getting the ContainerClient with Foundry
|
||||
|
||||
`AzureOpenAIClient.GetContainerClient()` is not supported and throws `InvalidOperationException`. Instead, use the project's OpenAI client which inherits directly from `OpenAI.OpenAIClient`:
|
||||
|
||||
```csharp
|
||||
// ❌ AzureOpenAIClient does not support ContainerClient
|
||||
var azureClient = new AzureOpenAIClient(endpoint, credential);
|
||||
azureClient.GetContainerClient(); // Throws InvalidOperationException
|
||||
|
||||
// ✅ Use AIProjectClient's project OpenAI client
|
||||
var containerClient = aiProjectClient.GetProjectOpenAIClient().GetContainerClient();
|
||||
await containerClient.DownloadContainerFileAsync("cntr_...", "cfile_...");
|
||||
```
|
||||
|
||||
The container ID and file ID are available from the `ContainerFileCitationMessageAnnotation` annotation in the response, accessible via `CitationAnnotation.RawRepresentation`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (`az login`)
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
|
||||
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Code Interpreter File Download with OpenAI](../../../02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/) — same scenario using Public OpenAI
|
||||
- [Code Interpreter](../Agent_Step14_CodeInterpreter/) — Code Interpreter without file download
|
||||
@@ -72,6 +72,7 @@ Some samples require extra tool-specific environment variables. See each sample
|
||||
| [Web search](./Agent_Step21_WebSearch/) | Web search tool |
|
||||
| [Memory search](./Agent_Step22_MemorySearch/) | Memory search tool |
|
||||
| [Local MCP](./Agent_Step23_LocalMCP/) | Local MCP client with HTTP transport |
|
||||
| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
|
||||
|
||||
## Running the samples
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
|
||||
/// <summary>
|
||||
@@ -25,6 +27,11 @@ public sealed record class ActionExecutorResult
|
||||
|
||||
internal static ActionExecutorResult ThrowIfNot(object? message)
|
||||
{
|
||||
if (message is PortableValue portableValue && portableValue.IsType(out ActionExecutorResult? unwrapped))
|
||||
{
|
||||
return unwrapped;
|
||||
}
|
||||
|
||||
if (message is not ActionExecutorResult executorMessage)
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ActionExecutorResult)})");
|
||||
|
||||
+4
-2
@@ -27,9 +27,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseA
|
||||
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
|
||||
}
|
||||
|
||||
public static bool RequiresInput(object? message) => message is ExternalInputRequest;
|
||||
public static bool RequiresInput(object? message) =>
|
||||
message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
|
||||
|
||||
public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
|
||||
public static bool RequiresNothing(object? message) =>
|
||||
message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
|
||||
|
||||
private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}");
|
||||
private AzureAgentInput? AgentInput => this.Model.Input;
|
||||
|
||||
+4
-2
@@ -46,12 +46,14 @@ internal sealed class InvokeMcpToolExecutor(
|
||||
/// <summary>
|
||||
/// Determines if the message indicates external input is required.
|
||||
/// </summary>
|
||||
public static bool RequiresInput(object? message) => message is ExternalInputRequest;
|
||||
public static bool RequiresInput(object? message) =>
|
||||
message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the message indicates no external input is required.
|
||||
/// </summary>
|
||||
public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
|
||||
public static bool RequiresNothing(object? message) =>
|
||||
message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool EmitResultEvent => false;
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that edge predicates correctly handle PortableValue-wrapped messages,
|
||||
/// which occur after checkpoint restore (JSON round-trip).
|
||||
/// </summary>
|
||||
public sealed class PortableValuePredicateTests
|
||||
{
|
||||
#region ActionExecutorResult.ThrowIfNot
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithDirectActionExecutorResult_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test-executor");
|
||||
|
||||
// Act
|
||||
ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(result);
|
||||
|
||||
// Assert
|
||||
actual.Should().BeSameAs(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedActionExecutorResult_Unwraps()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test-executor");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act
|
||||
ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(wrapped);
|
||||
|
||||
// Assert
|
||||
actual.ExecutorId.Should().Be("test-executor");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithNonActionExecutorResult_Throws()
|
||||
{
|
||||
// Arrange
|
||||
object message = "not an ActionExecutorResult";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithNull_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedNonResult_Throws()
|
||||
{
|
||||
// Arrange
|
||||
PortableValue wrapped = new("not an ActionExecutorResult");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(wrapped));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InvokeAzureAgentExecutor Predicates
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithDirectExternalInputRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(request).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
PortableValue wrapped = new(request);
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithActionExecutorResult_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(result).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithDirectActionExecutorResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(result).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithExternalInputRequest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(request).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InvokeMcpToolExecutor Predicates
|
||||
|
||||
[Fact]
|
||||
public void InvokeMcpToolExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
PortableValue wrapped = new(request);
|
||||
|
||||
// Act & Assert
|
||||
InvokeMcpToolExecutor.RequiresInput(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeMcpToolExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
InvokeMcpToolExecutor.RequiresNothing(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region QuestionExecutor.IsComplete
|
||||
|
||||
[Fact]
|
||||
public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NullResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange - result with null Result property means "complete"
|
||||
ActionExecutorResult result = new("test", result: null);
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
QuestionExecutor.IsComplete(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NonNullResult_ReturnsFalse()
|
||||
{
|
||||
// Arrange - result with non-null Result property means "not complete"
|
||||
ActionExecutorResult result = new("test", result: true);
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
QuestionExecutor.IsComplete(wrapped).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user