Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-verify-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ jobs:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
# TypeSafe (Jev)
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}

- name: Write Job Summary
if: always()
Expand Down
5 changes: 5 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<Project Path="samples/02-agents/AgentProviders/onnx/Agent_With_ONNX/Agent_With_ONNX.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
<Project Path="samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Agent_TypeSafe_Step01_LoopJudge.csproj" />
<Project Path="samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Agent_With_TypeSafe.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/DevUIAspireIntegration/">
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.AppHost/DevUIIntegration.AppHost.csproj" />
Expand Down Expand Up @@ -69,6 +71,7 @@
<Project Path="samples/02-agents/Agents/Agent_Step22_AgentMode/Agent_Step22_AgentMode.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step23_TodoList/Agent_Step23_TodoList.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step24_MultiModelRouting/Agent_Step24_MultiModelRouting.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Agent_Step25_ClassificationWithJev.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
Expand Down Expand Up @@ -631,6 +634,7 @@
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
<Project Path="src/Microsoft.Agents.AI.Tools.Shell/Microsoft.Agents.AI.Tools.Shell.csproj" />
<Project Path="src/Microsoft.Agents.AI.TypeSafe/Microsoft.Agents.AI.TypeSafe.csproj" />
<Project Path="src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
Expand Down Expand Up @@ -689,6 +693,7 @@
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/Microsoft.Agents.AI.Tools.Shell.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.TypeSafe.UnitTests/Microsoft.Agents.AI.TypeSafe.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions dotnet/agent-framework-src-and-tests.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
"src\\Microsoft.Agents.AI.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj",
"src\\Microsoft.Agents.AI.TypeSafe\\Microsoft.Agents.AI.TypeSafe.csproj",
"src\\Microsoft.Agents.AI.Valkey\\Microsoft.Agents.AI.Valkey.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Mcp\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj",
Expand Down Expand Up @@ -64,6 +65,7 @@
"tests\\Microsoft.Agents.AI.OpenAI.UnitTests\\Microsoft.Agents.AI.OpenAI.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Purview.UnitTests\\Microsoft.Agents.AI.Purview.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Tools.Shell.UnitTests\\Microsoft.Agents.AI.Tools.Shell.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.TypeSafe.UnitTests\\Microsoft.Agents.AI.TypeSafe.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.UnitTests\\Microsoft.Agents.AI.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Valkey.UnitTests\\Microsoft.Agents.AI.Valkey.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj",
Expand Down
66 changes: 66 additions & 0 deletions dotnet/eng/verify-samples/AgentsSamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,49 @@ internal static class AgentsSamples
],
},

new SampleDefinition
{
Name = "Agent_With_TypeSafe",
ProjectPath = "samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe",
RequiredEnvironmentVariables = ["TYPESAFE_API_KEY"],
Inputs = ["Y"],
MustContain =
[
"Direct JevChatClient result:",
"Agent result:",
"Default questions result:",
"Function calling result:",
"better_city:",
"Tool approval:",
"issue_refund(plan: Pro)",
],
ExpectedOutputDescription =
[
"The direct result should route the duplicate subscription charge to billing, and the agent result should route the failing checkout integration to technical with a high urgency probability.",
"The default questions result should route the invoice request to billing with a low urgency probability.",
"The function calling result should contain one weather line for Seattle and one for Amsterdam, with temperatures in Fahrenheit, followed by a better_city line that names one of the two cities.",
"The tool approval step should ask to approve issue_refund for the Pro plan and, after approval, show the refund result followed by a department line.",
"The output should not contain error messages or stack traces.",
],
},

new SampleDefinition
{
Name = "Agent_TypeSafe_Step01_LoopJudge",
ProjectPath = "samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge",
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "TYPESAFE_API_KEY"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain = ["Answer 1:", "Judge:", "red_sunsets:", "Final answer:"],
ExpectedOutputDescription =
[
"The first answer should explain only why the sky is blue, and the judge should mark red_sunsets as missing with a low probability.",
"A later answer should also explain why sunsets are red, and the judge should mark red_sunsets as met for it.",
"Every numbered answer (Answer 1, Answer 2, and so on) should be followed by a Judge block with probabilities. The Final answer section repeats the last judged answer and has no Judge block of its own.",
"The final answer should explain both why the sky is blue and why sunsets are red.",
"The output should not contain error messages or stack traces.",
],
},

// ── Agents ──────────────────────────────────────────────────────────

new SampleDefinition
Expand Down Expand Up @@ -403,6 +446,29 @@ internal static class AgentsSamples
],
},

new SampleDefinition
{
Name = "Agent_Step25_ClassificationWithJev",
ProjectPath = "samples/02-agents/Agents/Agent_Step25_ClassificationWithJev",
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "TYPESAFE_API_KEY"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain =
[
"Ticket:",
"Tool result:",
"The same tool, answered by a local keyword classifier:",
"local-keyword-classifier",
],
ExpectedOutputDescription =
[
"Each of the three tickets should show a tool result with department, urgent, and frustration answers from a jev model, followed by a one-line agent reply.",
"The failing Stripe integration ticket should be routed to technical and marked urgent, and the invoice request should be routed to billing and not urgent.",
"The vague account ticket may be sent to human review when the department confidence is low.",
"The last part should show a tool result from the local-keyword-classifier model for the invoice ticket, followed by an agent reply.",
"The output should not contain error messages or stack traces.",
],
},

// ── AgentSkills ─────────────────────────────────────────────────────

new SampleDefinition
Expand Down
7 changes: 7 additions & 0 deletions dotnet/samples/02-agents/AgentProviders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ covering basics, function tools, structured output, middleware, MCP, code interp
| [Conversation](./openai/Agent_OpenAI_Step05_Conversation/) | Multi-turn conversations with OpenAI |
| [Code Interpreter](./openai/Agent_OpenAI_Step06_CodeInterpreterFileDownload/) | Code interpreter with file downloads |

### [TypeSafe](./typesafe/)

| Sample | Description |
| --- | --- |
| [Agent with TypeSafe](./typesafe/Agent_With_TypeSafe/) | Use Jev, the TypeSafe System One model, as an IChatClient that answers typed questions and calls closed-set tools |
| [Loop judge](./typesafe/Agent_TypeSafe_Step01_LoopJudge/) | Use Jev as the judge of a LoopAgent, with one yes or no question per criterion |

## Running the samples

Navigate to a sample directory and run:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<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" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.TypeSafe\Microsoft.Agents.AI.TypeSafe.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.

// This sample uses Jev, the TypeSafe System One model, as the judge of a LoopAgent. A Foundry agent writes the
// answer. After each answer, Jev checks every criterion with its own yes or no question, and the loop sends the agent
// the criteria that are still missing, until the answer meets all of them.

#pragma warning disable MAAI001 // LoopAgent and DelegateLoopEvaluator are experimental.

using System.ClientModel;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.TypeSafe;
using Microsoft.Extensions.AI;

var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var typeSafeApiKey = Environment.GetEnvironmentVariable("TYPESAFE_API_KEY") ?? throw new InvalidOperationException("TYPESAFE_API_KEY is not set.");

// 1. The criteria. Each one becomes a Noul question, whose answer is the probability that the answer meets it, so the
// judge reports exactly which criteria are missing instead of a single verdict. The questions describe what a good
// answer contains; wording that steers the judge, such as "only say yes if", tends to push every answer one way.
Dictionary<string, string> criteria = new()
{
["blue_sky"] = "explains why the daytime sky appears blue",
["red_sunsets"] = "explains why sunsets appear red or orange",
["plain_language"] = "uses clear language suitable for a general audience",
};
Dictionary<string, JevQuestion> questions = criteria.ToDictionary(
criterion => criterion.Key,
criterion => (JevQuestion)new JevNoulQuestion { Instructions = $"Does the assistant's latest response {criterion.Value}?" });

// A criterion is met when its probability is above this threshold. 0.5 is not calibrated for any task: a higher value
// accepts fewer answers that still miss a criterion, at the cost of more loop runs. Choose it on labeled examples of
// your own answers.
const double CriterionThreshold = 0.5;

// 2. The agent that writes the answers. Its instructions hold back part of the first answer, so the judge has a gap
// to find and the loop runs more than once.
// 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.
AIAgent answerer = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()).AsAIAgent(
model: deploymentName,
name: "Answerer",
instructions: """
You answer science questions for a general audience in one short paragraph.
In your first answer, explain only why the daytime sky is blue, even if the user asks for more.
When feedback lists missing points, rewrite the whole answer so that it covers them too.
""");

// 3. The judge. AIJudgeLoopEvaluator asks its chat client for JSON structured output, which Jev cannot write, so a
// DelegateLoopEvaluator asks Jev the criteria questions instead. Jev reads the original request and the latest answer
// as a conversation.
using var judge = new JevChatClient(new ApiKeyCredential(typeSafeApiKey));
bool accepted = false;
var evaluator = new DelegateLoopEvaluator(async (context, cancellationToken) =>
{
Console.WriteLine($"\nAnswer {context.Iteration}:\n{context.LastResponse.Text}");
List<string> missing = await JudgeAsync(context.InitialMessages, context.LastResponse.Text, cancellationToken);
accepted = missing.Count == 0;

// The feedback names the missing criteria, which is the gap analysis that a generative judge would write.
return accepted
? LoopEvaluation.Stop()
: LoopEvaluation.Continue($"Your answer does not yet: {string.Join("; ", missing)}. Rewrite the whole answer so that it does.");
});

// 4. The loop re-runs the agent until the judge stops it. MaxIterations bounds the cost if the answer never passes.
const int MaxIterations = 3;
const string Request = "Explain why the sky is blue and why sunsets are red.";
AIAgent loop = new LoopAgent(answerer, evaluator, new LoopAgentOptions { MaxIterations = MaxIterations, NonStreamingReturnsLastResponseOnly = true });
AgentResponse response = await loop.RunAsync(Request);

// LoopAgent stops at MaxIterations before it calls the evaluator, so an answer from the last allowed run has not been
// judged yet. Judging it here keeps the final answer from being presented as accepted when it may miss a criterion.
if (!accepted)
{
Console.WriteLine($"\nAnswer {MaxIterations} (the last run the loop allows):\n{response.Text}");
accepted = (await JudgeAsync([new ChatMessage(ChatRole.User, Request)], response.Text, CancellationToken.None)).Count == 0;
}

Console.WriteLine(accepted ? "\nFinal answer:" : "\nFinal answer (some criteria are still missing):");
Console.WriteLine(response.Text);

// Asks Jev whether the answer meets each criterion, prints the probabilities, and returns the missing criteria.
async Task<List<string>> JudgeAsync(IEnumerable<ChatMessage> request, string answer, CancellationToken cancellationToken)
{
List<ChatMessage> conversation = [.. request, new ChatMessage(ChatRole.Assistant, answer)];
ChatResponse verdict = await judge.GetResponseAsync(conversation, new ChatOptions().WithJevQuestions(questions), cancellationToken);
JevResult result = verdict.GetJevResult()!;

Console.WriteLine("Judge:");
List<string> missing = [];
foreach ((string id, string description) in criteria)
{
double probability = ((JevNoulResponse)result.Answers[id]).Noul;
bool met = probability > CriterionThreshold;
Console.WriteLine($" {id}: {(met ? "met" : "missing")} (probability {probability:F2})");
if (!met)
{
missing.Add(description);
}
}

return missing;
}
Loading
Loading