mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34e728c56a | ||
|
|
5508081e14 | ||
|
|
b153dc8515 | ||
|
|
d84d7e348f | ||
|
|
fc9c81b0b1 | ||
|
|
cd1e3110aa | ||
|
|
e563849be3 | ||
|
|
9506fb28f6 | ||
|
|
3168eb4870 | ||
|
|
4452997e8d | ||
|
|
a39fd69f76 | ||
|
|
f3ea872156 | ||
|
|
e9b3a5bbc7 | ||
|
|
77e90e6013 | ||
|
|
65e77e52af | ||
|
|
e064f943ae | ||
|
|
1441fd903c | ||
|
|
a276c1295a | ||
|
|
2203fa0f8b | ||
|
|
1e350ea22f | ||
|
|
0c67dbbce5 | ||
|
|
a2856d3b92 | ||
|
|
69dcfe31ee | ||
|
|
be7b55f99b | ||
|
|
d37bf3fbcc | ||
|
|
8ed50009c6 | ||
|
|
6000b737e9 | ||
|
|
6a88c7b1a1 | ||
|
|
1b10b051fd | ||
|
|
8457533c69 | ||
|
|
b488158abe | ||
|
|
ff91473912 | ||
|
|
692fcd1888 | ||
|
|
7db6c4ab4e | ||
|
|
a2a672b687 | ||
|
|
654f099c42 | ||
|
|
ecabb5f592 | ||
|
|
b52136952f | ||
|
|
a427af91a9 | ||
|
|
235c578059 | ||
|
|
1fdc4be88d | ||
|
|
38f22ef006 | ||
|
|
65b6b28ffe | ||
|
|
7a88af0aef | ||
|
|
d249473a6d | ||
|
|
9f4c5f3faa | ||
|
|
0521f5bed8 | ||
|
|
a4c9e43afb | ||
|
|
f407f726a7 | ||
|
|
ac0e6b0ee1 | ||
|
|
ccff3d3452 | ||
|
|
35097d8c75 | ||
|
|
32ba81e990 | ||
|
|
84cb09cb68 | ||
|
|
a149aaa926 | ||
|
|
7dccf3a07b | ||
|
|
7e7d72275d | ||
|
|
f106a1a2b1 | ||
|
|
aa44e63074 | ||
|
|
e489ac0fa3 | ||
|
|
6c37ce8450 | ||
|
|
8ad66637d8 | ||
|
|
56603ab472 | ||
|
|
9f14a0f33f | ||
|
|
29af002c2e | ||
|
|
6eb251464b | ||
|
|
e3b4b6662b | ||
|
|
80cb6edc8d | ||
|
|
e4ca3e60f8 | ||
|
|
977c3adfb2 | ||
|
|
ad0dac3c86 | ||
|
|
390f93344c | ||
|
|
74ac470a56 | ||
|
|
5d355ac507 | ||
|
|
a17f13598b | ||
|
|
15256bb616 | ||
|
|
ac17adb595 | ||
|
|
0f3f4dbcaf | ||
|
|
09f59b21ad | ||
|
|
c609b14f63 | ||
|
|
f96772f6e8 | ||
|
|
1f8e70d7ad | ||
|
|
3dc59c83b5 | ||
|
|
d1205896a1 | ||
|
|
ec82ed15d2 | ||
|
|
2b66ca03b2 | ||
|
|
aa88195dcd | ||
|
|
eaad042241 | ||
|
|
de80543302 | ||
|
|
9e51e2f0bc | ||
|
|
0daa7700c6 | ||
|
|
10afb86213 | ||
|
|
4e25917644 | ||
|
|
a971d24f1e | ||
|
|
907654a489 | ||
|
|
5e565dbec0 | ||
|
|
a2d1e69652 | ||
|
|
de78348d76 | ||
|
|
e8902c0d11 | ||
|
|
6255abd687 | ||
|
|
d742364d81 | ||
|
|
2c2800aad4 | ||
|
|
838a7fd61d | ||
|
|
ef798629e5 | ||
|
|
5c6cf4fc92 | ||
|
|
06d43ee130 | ||
|
|
96d3f2a55e | ||
|
|
f56218fa1e | ||
|
|
8d939f8ffa | ||
|
|
6fdf6111e6 | ||
|
|
405bd6fb4b | ||
|
|
73033c300f | ||
|
|
98cd72839e | ||
|
|
184ee9d518 | ||
|
|
2f7250fe0f | ||
|
|
493891620d | ||
|
|
f3e0be9555 |
@@ -1,69 +1,19 @@
|
||||
# GitHub Copilot Instructions
|
||||
|
||||
This repository contains both Python and C# code.
|
||||
All python code resides under the `python/` directory.
|
||||
All C# code resides under the `dotnet/` directory.
|
||||
Microsoft Agent Framework - a multi-language framework for building, orchestrating, and deploying AI agents.
|
||||
|
||||
The purpose of the code is to provide a framework for building AI agents.
|
||||
## Repository Structure
|
||||
|
||||
When contributing to this repository, please follow these guidelines:
|
||||
- `python/` - Python implementation → see [python/AGENTS.md](../python/AGENTS.md)
|
||||
- `dotnet/` - C#/.NET implementation → see [dotnet/AGENTS.md](../dotnet/AGENTS.md)
|
||||
- `docs/` - Design documents and architectural decision records
|
||||
|
||||
## C# Code Guidelines
|
||||
## Architectural Decision Records (ADRs)
|
||||
|
||||
Here are some general guidelines that apply to all code.
|
||||
ADRs in `docs/decisions/` capture significant design decisions and their rationale. They document considered alternatives, trade-offs, and the reasoning behind choices.
|
||||
|
||||
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
- All public methods and classes should have XML documentation comments.
|
||||
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
|
||||
- After adding or modifying code, run `dotnet format` to automatically fix any formatting errors.
|
||||
**Templates:**
|
||||
- `adr-template.md` - Full template with detailed sections
|
||||
- `adr-short-template.md` - Abbreviated template for simpler decisions
|
||||
|
||||
### C# Sample Code Guidelines
|
||||
|
||||
Sample code is located in the `dotnet/samples` directory.
|
||||
|
||||
When adding a new sample, follow these steps:
|
||||
|
||||
- The sample should be a standalone .net project in one of the subdirectories of the samples directory.
|
||||
- The directory name should be the same as the project name.
|
||||
- The directory should contain a README.md file that explains what the sample does and how to run it.
|
||||
- The README.md file should follow the same format as other samples.
|
||||
- The csproj file should match the directory name.
|
||||
- The csproj file should be configured in the same way as other samples.
|
||||
- The project should preferably contain a single Program.cs file that contains all the sample code.
|
||||
- The sample should be added to the solution file in the samples directory.
|
||||
- The sample should be tested to ensure it works as expected.
|
||||
- A reference to the new samples should be added to the README.md file in the parent directory of the new sample.
|
||||
|
||||
The sample code should follow these guidelines:
|
||||
|
||||
- Configuration settings should be read from environment variables, e.g. `var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");`.
|
||||
- Environment variables should use upper snake_case naming convention.
|
||||
- Secrets should not be hardcoded in the code or committed to the repository.
|
||||
- The code should be well-documented with comments explaining the purpose of each step.
|
||||
- The code should be simple and to the point, avoiding unnecessary complexity.
|
||||
- Prefer inline literals over constants for values that are not reused. For example, use `new ChatClientAgent(chatClient, instructions: "You are a helpful assistant.")` instead of defining a constant for "instructions".
|
||||
- Ensure that all private classes are sealed
|
||||
- Use the Async suffix on the name of all async methods that return a Task or ValueTask.
|
||||
- Prefer defining variables using types rather than var, to help users understand the types involved.
|
||||
- Follow the patterns in the samples in the same directories where new samples are being added.
|
||||
- The structure of the sample should be as follows:
|
||||
- The top of the Program.cs should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
- Then add a comment describing what the sample is demonstrating.
|
||||
- Then add the necessary using statements.
|
||||
- Then add the main code logic.
|
||||
- Finally, add any helper methods or classes at the bottom of the file.
|
||||
|
||||
### C# Unit Test Guidelines
|
||||
|
||||
Unit tests are located in the `dotnet/tests` directory in projects with a `.UnitTests.csproj` suffix.
|
||||
|
||||
Unit tests should follow these guidelines:
|
||||
|
||||
- Use `this.` for accessing class members
|
||||
- Add Arrange, Act and Assert comments for each test
|
||||
- Ensure that all private classes, that are not subclassed, are sealed
|
||||
- Use the Async suffix on the name of all async methods
|
||||
- Use the Moq library for mocking objects where possible
|
||||
- Validate that each test actually tests the target behavior, e.g. we should not have tests that creates a mock, calls the mock and then verifies that the mock was called, without the target code being involved. We also shouldn't have tests that test language features, e.g. something that the compiler would catch anyway.
|
||||
- Avoid adding excessive comments to tests. Instead favour clear easy to understand code.
|
||||
- Follow the patterns in the unit tests in the same project or classes to which new tests are being added
|
||||
When proposing architectural changes, create an ADR to capture options considered and the decision rationale. See [docs/decisions/README.md](../docs/decisions/README.md) for the full process.
|
||||
|
||||
@@ -30,9 +30,11 @@ from dataclasses import dataclass
|
||||
# =============================================================================
|
||||
ENFORCED_MODULES: set[str] = {
|
||||
"packages.azure-ai.agent_framework_azure_ai",
|
||||
"packages.core.agent_framework",
|
||||
"packages.core.agent_framework._workflows",
|
||||
"packages.purview.agent_framework_purview",
|
||||
# Add more modules here as coverage improves:
|
||||
# "packages.core.agent_framework",
|
||||
# "packages.core.agent_framework._workflows",
|
||||
# "packages.azure-ai-search.agent_framework_azure_ai_search",
|
||||
# "packages.anthropic.agent_framework_anthropic",
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Checks
|
||||
pre-commit-hooks:
|
||||
name: Pre-commit Hooks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.14"]
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
@@ -37,16 +37,106 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
name: Run Pre-Commit Hooks
|
||||
path: ~/.cache/prek
|
||||
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: j178/prek-action@v1
|
||||
name: Run Pre-commit Hooks (excluding poe-check)
|
||||
env:
|
||||
SKIP: poe-check
|
||||
with:
|
||||
extra_args: --config python/.pre-commit-config.yaml --all-files
|
||||
extra-args: --cd python --all-files
|
||||
|
||||
package-checks:
|
||||
name: Package Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run fmt, lint, pyright in parallel across packages
|
||||
run: uv run poe check-packages
|
||||
|
||||
samples-markdown:
|
||||
name: Samples & Markdown
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run samples lint
|
||||
run: uv run poe samples-lint
|
||||
- name: Run samples syntax check
|
||||
run: uv run poe samples-syntax
|
||||
- name: Run markdown code lint
|
||||
run: uv run poe markdown-code-lint
|
||||
|
||||
mypy:
|
||||
name: Mypy Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run Mypy
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
|
||||
|
||||
@@ -96,8 +96,7 @@ jobs:
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 900 --retries 3 --retry-delay 10
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
|
||||
working-directory: ./python
|
||||
- name: Test core samples
|
||||
timeout-minutes: 10
|
||||
@@ -153,8 +152,8 @@ jobs:
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
timeout-minutes: 15
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
|
||||
working-directory: ./python
|
||||
- name: Test Azure AI samples
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -199,8 +199,6 @@ temp*/
|
||||
.tmp/
|
||||
.temp/
|
||||
|
||||
agents.md
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
WARP.md
|
||||
|
||||
@@ -53,7 +53,7 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
### ✨ **Highlights**
|
||||
|
||||
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, human-in-the-loop, and time-travel capabilities
|
||||
- [Python workflows](./python/samples/getting_started/workflows/) | [.NET workflows](./dotnet/samples/GettingStarted/Workflows/)
|
||||
- [Python workflows](./python/samples/03-workflows/) | [.NET workflows](./dotnet/samples/GettingStarted/Workflows/)
|
||||
- **AF Labs**: Experimental packages for cutting-edge features including benchmarking, reinforcement learning, and research initiatives
|
||||
- [Labs directory](./python/packages/lab/)
|
||||
- **DevUI**: Interactive developer UI for agent development, testing, and debugging workflows
|
||||
@@ -73,11 +73,11 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
|
||||
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
|
||||
- **Observability**: Built-in OpenTelemetry integration for distributed tracing, monitoring, and debugging
|
||||
- [Python observability](./python/samples/getting_started/observability/) | [.NET telemetry](./dotnet/samples/GettingStarted/AgentOpenTelemetry/)
|
||||
- [Python observability](./python/samples/02-agents/observability/) | [.NET telemetry](./dotnet/samples/GettingStarted/AgentOpenTelemetry/)
|
||||
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
|
||||
- [Python examples](./python/samples/getting_started/agents/) | [.NET examples](./dotnet/samples/GettingStarted/AgentProviders/)
|
||||
- [Python examples](./python/samples/02-agents/providers/) | [.NET examples](./dotnet/samples/GettingStarted/AgentProviders/)
|
||||
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
|
||||
- [Python middleware](./python/samples/getting_started/middleware/) | [.NET middleware](./dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/)
|
||||
- [Python middleware](./python/samples/02-agents/middleware/) | [.NET middleware](./dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/)
|
||||
|
||||
### đź’¬ **We want your feedback!**
|
||||
|
||||
@@ -108,7 +108,7 @@ async def main():
|
||||
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
||||
# api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential
|
||||
credential=AzureCliCredential(), # Optional, if using api_key
|
||||
).create_agent(
|
||||
).as_agent(
|
||||
name="HaikuBot",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
)
|
||||
@@ -131,7 +131,7 @@ using OpenAI;
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
@@ -150,7 +150,7 @@ var agent = new OpenAIClient(
|
||||
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
|
||||
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
@@ -159,9 +159,9 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
||||
|
||||
### Python
|
||||
|
||||
- [Getting Started with Agents](./python/samples/getting_started/agents): basic agent creation and tool usage
|
||||
- [Chat Client Examples](./python/samples/getting_started/chat_client): direct chat client usage patterns
|
||||
- [Getting Started with Workflows](./python/samples/getting_started/workflows): basic workflow creation and integration with agents
|
||||
- [Getting Started with Agents](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
|
||||
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
|
||||
- [Getting Started with Workflows](./python/samples/03-workflows): workflow creation and integration with agents
|
||||
|
||||
### .NET
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Declarative Agents
|
||||
|
||||
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
|
||||
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/02-agents/declarative/).
|
||||
|
||||
@@ -126,4 +126,4 @@ response = await client.get_response(
|
||||
|
||||
Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.
|
||||
|
||||
See [typed_options.py](../../python/samples/getting_started/chat_client/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
|
||||
See [typed_options.py](../../python/samples/02-agents/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: westey-m
|
||||
date: 2026-01-27
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# AgentRunContext for Agent Run
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
During an agent run, various components involved in the execution (middleware, filters, tools, nested agents, etc.) may need access to contextual information about the current run, such as:
|
||||
|
||||
1. The agent that is executing the run
|
||||
2. The session associated with the run
|
||||
3. The request messages passed to the agent
|
||||
4. The run options controlling the agent's behavior
|
||||
|
||||
Additionally, some components may need to modify this context during execution, for example:
|
||||
|
||||
- Replacing the session with a different one
|
||||
- Modifying the request messages before they reach the agent core
|
||||
- Updating or replacing the run options entirely
|
||||
|
||||
Currently, there is no standardized way to access or modify this context from arbitrary code that executes during an agent run, especially from deeply nested call stacks where the context is not explicitly passed.
|
||||
|
||||
## Sample Scenario
|
||||
|
||||
When using an Agent as an AIFunction developers may want to pass context from the parent agent run to the child agent run. For example, the developer may want to copy chat history to the child agent, or share the same session across both agents.
|
||||
|
||||
To enable these scenarios, we need a way to access the parent agent run context, including e.g. the parent agent itself, the parent agent session, and the parent run options from function tool calls.
|
||||
|
||||
```csharp
|
||||
public static AIFunction AsAIFunctionWithSessionPropagation(this ChatClientAgent agent, AIFunctionFactoryOptions? options = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
|
||||
[Description("Invoke an agent to retrieve some information.")]
|
||||
async Task<string> InvokeAgentAsync(
|
||||
[Description("Input query to invoke the agent.")] string query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the session from the parent agent and pass it to the child agent.
|
||||
var session = AIAgent.CurrentRunContext?.Session;
|
||||
|
||||
// Alternatively, the developer may want to create a new session but copy over the chat history from the parent agent.
|
||||
// var parentChatHistory = AIAgent.CurrentRunContext?.Session?.GetService<IList<ChatMessage>>();
|
||||
// if (parentChatHistory != null)
|
||||
// {
|
||||
// var chp = new InMemoryChatHistoryProvider();
|
||||
// foreach (var message in parentChatHistory)
|
||||
// {
|
||||
// chp.Add(message);
|
||||
// }
|
||||
// session = agent.GetNewSession(chp);
|
||||
// }
|
||||
|
||||
var response = await agent.RunAsync(query, session: session, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
options ??= new();
|
||||
options.Name ??= SanitizeAgentName(agent.Name);
|
||||
options.Description ??= agent.Description;
|
||||
|
||||
return AIFunctionFactory.Create(InvokeAgentAsync, options);
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Components executing during an agent run need access to run context without explicit parameter passing through every layer
|
||||
- Context should flow naturally across async calls without manual propagation
|
||||
- The design should allow modification of context properties by agent decorators (e.g., replacing options or session)
|
||||
- Solution should be consistent with patterns used in similar frameworks (e.g., `FunctionInvokingChatClient.CurrentContext` `HttpContext.Current`, `Activity.Current`)
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Option 1**: Pass context explicitly through all method signatures
|
||||
- **Option 2**: Use `AsyncLocal<T>` to provide ambient context accessible anywhere during the run
|
||||
- **Option 3**: Use a combination of explicit parameters for `RunCoreAsync` and `AsyncLocal<T>` for ambient access
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **Option 3** - Combination of explicit parameters and AsyncLocal ambient access.
|
||||
|
||||
This approach provides the best of both worlds:
|
||||
|
||||
1. **Explicit parameters are passed to `RunCoreAsync`**: The core agent implementation receives the parameters explicitly, making it clear what data is available and enabling easy unit testing. Any modification of these in a decorator will require calling `RunAsync` on the inner agent with the updated parameters, which would result in the inner agent creating a new `AgentRunContext` instance.
|
||||
|
||||
```csharp
|
||||
public async Task<AgentResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
|
||||
return await this.RunCoreAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
```
|
||||
|
||||
2. **`AsyncLocal<AgentRunContext?>` for ambient access**: The context is stored in an `AsyncLocal<T>` field, making it accessible from any code executing during the agent run via a static property.
|
||||
|
||||
The main scenario for this is to allow deeply nested components (e.g., tools, chat client middleware) to access the context without needing to pass it through every method signature. These are external components that cannot easily be modified to accept additional parameters. For internal components, we prefer passing any parameters explicitly.
|
||||
|
||||
```csharp
|
||||
public static AgentRunContext? CurrentRunContext
|
||||
{
|
||||
get => s_currentContext.Value;
|
||||
protected set => s_currentContext.Value = value;
|
||||
}
|
||||
```
|
||||
|
||||
### AgentRunContext Design
|
||||
|
||||
The `AgentRunContext` class encapsulates all run-related state:
|
||||
|
||||
```csharp
|
||||
public class AgentRunContext
|
||||
{
|
||||
public AgentRunContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IReadOnlyCollection<ChatMessage> requestMessages,
|
||||
AgentRunOptions? agentRunOptions)
|
||||
|
||||
public AIAgent Agent { get; }
|
||||
public AgentSession? Session { get; }
|
||||
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
|
||||
public AgentRunOptions? RunOptions { get; }
|
||||
}
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
|
||||
- **All properties are read-only**: While some of the sub-properties on the provided properties (like `AgentRunOptions.AllowBackgroundResponses`) may be mutable, the `AgentRunContext` itself is immutable and we want to discourage anyone modifying the values in the context. Modifying the context is unlikely to result in the desired behavior, as the values will typically already have been used by the time any custom code accesses them.
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Ambient Access**: Any code executing during the run can access context via `AIAgent.CurrentRunContext` without needing explicit parameters
|
||||
2. **Async Flow**: `AsyncLocal<T>` automatically flows across async/await boundaries
|
||||
3. **Modifiability**: Components can modify or replace session, messages, or options as needed
|
||||
4. **Testability**: The explicit parameter to `RunCoreAsync` makes unit testing straightforward
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,658 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: sergeymenshykh
|
||||
date: 2026-01-22
|
||||
deciders: rbarreto, westey-m, stephentoub
|
||||
informed: {}
|
||||
---
|
||||
|
||||
# Structured Output
|
||||
|
||||
Structured output is a valuable aspect of any agent system, since it forces an agent to produce output in a required format that may include required fields.
|
||||
This allows easily turning unstructured data into structured data using a general-purpose language model.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Structured output is currently supported only by `ChatClientAgent` and can be configured in two ways:
|
||||
|
||||
**Approach 1: ResponseFormat + Deserialize**
|
||||
|
||||
Specify the SO type schema via the `ChatClientAgent{Run}Options.ChatOptions.ResponseFormat` property at agent creation or invocation time, then use `JsonSerializer.Deserialize<T>` to extract the structured data from the response text.
|
||||
|
||||
```csharp
|
||||
// SO type can be provided at agent creation time
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "...",
|
||||
ChatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...");
|
||||
|
||||
PersonInfo personInfo = response.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
|
||||
// Alternatively, SO type can be provided at agent invocation time
|
||||
response = await agent.RunAsync("...", new ChatClientAgentRunOptions()
|
||||
{
|
||||
ChatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
|
||||
personInfo = response.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
```
|
||||
|
||||
**Approach 2: Generic RunAsync<T>**
|
||||
|
||||
Supply the SO type as a generic parameter to `RunAsync<T>` and access the parsed result directly via the `Result` property.
|
||||
|
||||
```csharp
|
||||
ChatClientAgent agent = ...;
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("...");
|
||||
|
||||
Console.WriteLine($"Name: {response.Result.Name}");
|
||||
Console.WriteLine($"Age: {response.Result.Age}");
|
||||
Console.WriteLine($"Occupation: {response.Result.Occupation}");
|
||||
```
|
||||
Note: `RunAsync<T>` is an instance method of `ChatClientAgent` and not part of the `AIAgent` base class since not all agents support structured output.
|
||||
|
||||
Approach 1 is perceived as cumbersome by the community, as it requires additional effort when using primitive or collection types - the SO schema may need to be wrapped in an artificial JSON object. Otherwise, the caller will encounter an error like _Invalid schema for response_format 'Movie': schema must be a JSON Schema of 'type: "object"', got 'type: "array"'_.
|
||||
This occurs because OpenAI and compatible APIs require a JSON object as the root schema.
|
||||
|
||||
Approach 1 is also necessary in scenarios where (a) agents can only be configured with SO at creation time (such as with `AIProjectClient`), (b) the SO type is not known at compile time, or (c) the JSON schema is represented as text (for declarative agents) or as a `JsonElement`.
|
||||
|
||||
Approach 2 is more convenient and works seamlessly with primitives and collections. However, it requires the SO type to be known at compile time, making it less flexible.
|
||||
|
||||
Additionally, since the `RunAsync<T>` methods are instance methods of `ChatClientAgent` and are not part of the `AIAgent` base class, applying decorators like `OpenTelemetryAgent` on top of `ChatClientAgent` prevents users from accessing `RunAsync<T>`, meaning structured output is not available with decorated agents.
|
||||
|
||||
Given the different scenarios above in which structured output can be used, there is no one-size-fits-all solution. Each approach has its own advantages and limitations,
|
||||
and the two can complement each other to provide a comprehensive structured output experience across various use cases.
|
||||
|
||||
## Approaches Overview
|
||||
|
||||
1. SO usage via `ResponseFormat` property
|
||||
2. SO usage via `RunAsync<T>` generic method
|
||||
|
||||
## 1. SO usage via `ResponseFormat` property
|
||||
|
||||
This approach should be used in the following scenarios:
|
||||
- 1.1 SO result as text is sufficient as is, and deserialization is not required
|
||||
- 1.2 SO for inter-agent collaboration
|
||||
- 1.3 SO can only be configured at agent creation time (such as with `AIProjectClient`)
|
||||
- 1.4 SO type is not known at compile time and represented by System.Type
|
||||
- 1.5 SO is represented by JSON schema and there's no corresponding .NET type either at compile time or at runtime
|
||||
- 1.6 SO in streaming scenarios, where the SO response is produced in parts
|
||||
|
||||
**Note: Primitives and arrays are not supported by this approach.**
|
||||
|
||||
When a caller provides a schema via `ResponseFormat`, they are explicitly telling the framework what schema to use. The framework passes that schema through as-is and
|
||||
is not responsible for transforming it. Because the framework does not own the schema, it cannot wrap primitives or arrays into a JSON object to satisfy API requirements,
|
||||
nor can it unwrap the response afterward - the caller controls the schema and is responsible for ensuring it is compatible with the underlying API.
|
||||
|
||||
This is in contrast to the `RunAsync<T>` approach (section 2), where the caller provides a type `T` and says "make it work." In that case, the caller does not
|
||||
dictate the schema - the framework infers the schema from `T`, owns the end-to-end pipeline (schema generation, API invocation, and deserialization), and can
|
||||
therefore wrap and unwrap primitives and arrays transparently.
|
||||
|
||||
Additionally, in streaming scenarios (1.6), the framework cannot reliably unwrap a response it did not wrap, since it has no way of knowing whether the caller wrapped the schema.Wrapping and unwrapping can only be done safely when the framework owns the entire lifecycle - from schema creation through deserialization — which is only the case with `RunAsync<T>`.
|
||||
|
||||
If a caller needs to work with primitives or arrays via the `ResponseFormat` approach, they can easily create a wrapper type around them:
|
||||
|
||||
```csharp
|
||||
public class MovieListWrapper
|
||||
{
|
||||
public List<string> Movies { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 1.1 SO result as text is sufficient as is, and deserialization is not required
|
||||
|
||||
In this scenario, the caller only needs the raw JSON text returned by the model and does not need to deserialize it into a .NET type.
|
||||
The SO schema is specified via `ResponseFormat` at agent creation or invocation time, and the response text is consumed directly from the `AgentResponse`.
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent();
|
||||
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
|
||||
};
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...", options: runOptions);
|
||||
|
||||
Console.WriteLine(response.Text);
|
||||
```
|
||||
|
||||
### 1.2 SO for inter-agent collaboration
|
||||
|
||||
This scenario assumes a multi-agent setup where agents collaborate by passing messages to each other.
|
||||
One agent produces structured output as text that is then passed directly as input to the next agent, without intermediate deserialization.
|
||||
|
||||
```csharp
|
||||
// First agent extracts structured data from unstructured input
|
||||
AIAgent extractionAgent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "ExtractionAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "Extract person information from the provided text.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
|
||||
}
|
||||
});
|
||||
|
||||
AgentResponse extractionResponse = await extractionAgent.RunAsync("John Smith is a 35-year-old software engineer.");
|
||||
|
||||
// Pass the message with structured output text directly to the next agent
|
||||
ChatMessage soMessage = extractionResponse.Messages.Last();
|
||||
|
||||
AIAgent summaryAgent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "SummaryAgent",
|
||||
ChatOptions = new() { Instructions = "Given the following structured person data, write a short professional bio." }
|
||||
});
|
||||
|
||||
AgentResponse summaryResponse = await summaryAgent.RunAsync(soMessage);
|
||||
|
||||
Console.WriteLine(summaryResponse);
|
||||
```
|
||||
|
||||
### 1.3 SO configured at agent creation time
|
||||
|
||||
In this scenario, the SO schema can only be configured at agent creation time (such as with `AIProjectClient`) and cannot be changed on a per-run basis.
|
||||
The caller specifies the `ResponseFormat` when creating the agent, and all subsequent invocations use the same schema.
|
||||
|
||||
```csharp
|
||||
AIProjectClient client = ...;
|
||||
|
||||
AIAgent agent = await client.CreateAIAgentAsync(model: "<model>", new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "...",
|
||||
ChatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
|
||||
AgentResponse response = await agent.RunAsync("Please provide information about John Smith.");
|
||||
|
||||
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>(response.Text, JsonSerializerOptions.Web)!;
|
||||
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
```
|
||||
|
||||
### 1.4 SO type not known at compile time and represented by System.Type
|
||||
|
||||
In this scenario, the SO type is not known at compile time and is provided as a `System.Type` at runtime. This is useful for dynamic scenarios where the schema is determined programmatically,
|
||||
such as when building tooling or frameworks that work with user-defined types.
|
||||
|
||||
```csharp
|
||||
Type soType = GetStructuredOutputTypeFromConfiguration(); // e.g., typeof(PersonInfo)
|
||||
|
||||
ChatResponseFormat responseFormat = ChatResponseFormat.ForJsonSchema(soType);
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...", new ChatClientAgentRunOptions()
|
||||
{
|
||||
ChatOptions = new() { ResponseFormat = responseFormat }
|
||||
});
|
||||
|
||||
PersonInfo personInfo = (PersonInfo)JsonSerializer.Deserialize(response.Text, soType, JsonSerializerOptions.Web)!;
|
||||
```
|
||||
|
||||
### 1.5 SO represented by JSON schema with no corresponding .NET type
|
||||
|
||||
In this scenario, the SO schema is represented as raw JSON schema text or a `JsonElement`, and there is no corresponding .NET type available at compile time or runtime.
|
||||
This is typical for declarative agents or scenarios where schemas are loaded from external configuration.
|
||||
|
||||
```csharp
|
||||
// JSON schema provided as a string, e.g., loaded from a configuration file
|
||||
string jsonSchema = """
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"age": { "type": "integer" },
|
||||
"occupation": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "age", "occupation"]
|
||||
}
|
||||
""";
|
||||
|
||||
ChatResponseFormat responseFormat = ChatResponseFormat.ForJsonSchema(
|
||||
jsonSchemaName: "PersonInfo",
|
||||
jsonSchema: BinaryData.FromString(jsonSchema));
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...", new ChatClientAgentRunOptions()
|
||||
{
|
||||
ChatOptions = new() { ResponseFormat = responseFormat }
|
||||
});
|
||||
|
||||
// Consume the SO result as text since there's no .NET type to deserialize into
|
||||
Console.WriteLine(response.Text);
|
||||
```
|
||||
|
||||
### 1.6 SO in streaming scenarios
|
||||
|
||||
In this scenario, the SO response is produced incrementally in parts via streaming. The caller specifies the `ResponseFormat` and consumes the response chunks as they arrive.
|
||||
Deserialization is performed after all chunks have been received.
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "HelpfulAssistant",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
|
||||
}
|
||||
});
|
||||
|
||||
IAsyncEnumerable<AgentResponseUpdate> updates = agent.RunStreamingAsync("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
|
||||
AgentResponse response = await updates.ToAgentResponseAsync();
|
||||
|
||||
// Deserialize the complete SO result after streaming is finished
|
||||
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>(response.Text)!;
|
||||
```
|
||||
|
||||
## 2. SO usage via `RunAsync<T>` generic method
|
||||
|
||||
This approach provides a convenient way to work with structured output on a per-run basis when the target type is known at compile time and a typed instance of the result
|
||||
is required.
|
||||
|
||||
### Decision Drivers
|
||||
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
### Considered Options
|
||||
|
||||
1. `RunAsync<T>` as an instance method of `AIAgent` class delegating to virtual `RunCoreAsync<T>`
|
||||
2. `RunAsync<T>` as an extension method using feature collection
|
||||
3. `RunAsync<T>` as a method of the new `ITypedAIAgent` interface
|
||||
4. `RunAsync<T>` as an instance method of `AIAgent` class working via the new `AgentRunOptions.ResponseFormat` property
|
||||
|
||||
### 1. `RunAsync<T>` as an instance method of `AIAgent` class delegating to virtual `RunCoreAsync<T>`
|
||||
|
||||
This option adds the `RunAsync<T>` method directly to the `AIAgent` base class.
|
||||
|
||||
```csharp
|
||||
public abstract class AIAgent
|
||||
{
|
||||
public Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this.RunCoreAsync<T>(messages, session, serializerOptions, options, cancellationToken);
|
||||
|
||||
protected virtual Task<AgentResponse<T>> RunCoreAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException($"The agent of type '{this.GetType().FullName}' does not support typed responses.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agents with native SO support override the `RunCoreAsync<T>` method to provide their implementation. If not overridden, the method throws a `NotSupportedException`.
|
||||
|
||||
Users will call the generic `RunAsync<T>` method directly on the agent:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
```
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- The `AIAgent.RunAsync<T>` method is easily discoverable.
|
||||
- Both the SO decorator and `ChatClientAgent` have compile-time access to the type `T`, allowing them to use the native `IChatClient.GetResponseAsync<T>` API, which handles primitives and collections seamlessly.
|
||||
|
||||
Cons:
|
||||
- Agents without native SO support will still expose `RunAsync<T>`, which may be misleading.
|
||||
- `ChatClientAgent` exposing `RunAsync<T>` may be misleading when the underlying chat client does not support SO.
|
||||
- All `AIAgent` decorators must override `RunCoreAsync<T>` to properly handle `RunAsync<T>` calls.
|
||||
|
||||
### 2. `RunAsync<T>` as an extension method using feature collection
|
||||
|
||||
This option uses the Agent Framework feature collection (implemented via `AgentRunOptions.AdditionalProperties`) to pass a `StructuredOutputFeature` to agents, signaling that SO is requested.
|
||||
|
||||
Agents with native SO support check for this feature. If present, they read the target type, build the schema, invoke the underlying API, and store the response back in the feature.
|
||||
```csharp
|
||||
public class StructuredOutputFeature
|
||||
{
|
||||
public StructuredOutputFeature(Type outputType)
|
||||
{
|
||||
this.OutputType = outputType;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Type OutputType { get; set; }
|
||||
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
|
||||
public AgentResponse? Response { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
The `RunAsync<T>` extension method for `AIAgent` adds this feature to the collection.
|
||||
```csharp
|
||||
public static async Task<AgentResponse<T>> RunAsync<T>(
|
||||
this AIAgent agent,
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create the structured output feature.
|
||||
StructuredOutputFeature structuredOutputFeature = new(typeof(T))
|
||||
{
|
||||
SerializerOptions = serializerOptions,
|
||||
};
|
||||
|
||||
// Register it in the feature collection.
|
||||
((options ??= new AgentRunOptions()).AdditionalProperties ??= []).Add(typeof(StructuredOutputFeature).FullName!, structuredOutputFeature);
|
||||
|
||||
var response = await agent.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (structuredOutputFeature.Response is not null)
|
||||
{
|
||||
return new StructuredOutputResponse<T>(structuredOutputFeature.Response, response, serializerOptions);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No structured output response was generated by the agent.");
|
||||
}
|
||||
```
|
||||
|
||||
Users will call the `RunAsync<T>` extension method directly on the agent:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
```
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- The `RunAsync<T>` extension method is easily discoverable.
|
||||
- The `AIAgent` public API surface remains unchanged.
|
||||
- No changes required to `AIAgent` decorators.
|
||||
|
||||
Cons:
|
||||
- Agents without native SO support will still expose `RunAsync<T>`, which may be misleading.
|
||||
- `ChatClientAgent` exposing `RunAsync<T>` may be misleading when the underlying chat client does not support SO.
|
||||
|
||||
### 3. `RunAsync<T>` as a method of the new `ITypedAIAgent` interface
|
||||
|
||||
This option defines a new `ITypedAIAgent` interface that agents with SO support implement. Agents without SO support do not implement it, allowing users to check for SO capability via interface detection.
|
||||
|
||||
The interface:
|
||||
```csharp
|
||||
public interface ITypedAIAgent
|
||||
{
|
||||
Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Agents with SO support implement this interface:
|
||||
```csharp
|
||||
public sealed partial class ChatClientAgent : AIAgent, ITypedAIAgent
|
||||
{
|
||||
public async Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, `ChatClientAgent` presents a challenge: it can work with chat clients that either support or do not support SO. Implementing the interface does not guarantee
|
||||
the underlying chat client supports SO, which undermines the core idea of using interface detection to determine SO capability.
|
||||
|
||||
Additionally, to allow users to access interface methods on decorated agents, all decorators must implement `ITypedAIAgent`. This makes it difficult for users to
|
||||
determine whether the underlying agent actually supports SO, further weakening the purpose of this approach.
|
||||
|
||||
Furthermore, users would have to probe the agent type to check if it implements the `ITypedAIAgent` interface and cast it accordingly to access the `RunAsync<T>` methods.
|
||||
This adds friction to the user experience. A `RunAsync<T>` extension method for `AIAgent` could be provided to alleviate that.
|
||||
|
||||
Given these drawbacks, this option is more complex to implement than the others without providing clear benefits.
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- Both the SO decorator and `ChatClientAgent` have compile-time access to the type `T`, allowing them to use the native `IChatClient.GetResponseAsync<T>` API, which handles primitives and collections seamlessly.
|
||||
|
||||
Cons:
|
||||
- `ChatClientAgent` implementing `ITypedAIAgent` may be misleading when the underlying chat client does not support SO.
|
||||
- All `AIAgent` decorators must implement `ITypedAIAgent` to handle `RunAsync<T>` calls.
|
||||
- Decorators implementing the interface may mislead users into thinking the underlying agent natively supports SO.
|
||||
- Agents must implement all members of `ITypedAIAgent`, not just a core method.
|
||||
- Users must check the agent type and cast to `ITypedAIAgent` to access `RunAsync<T>`.
|
||||
|
||||
### 4. `RunAsync<T>` as an instance method of `AIAgent` class working via the new `AgentRunOptions.ResponseFormat` property
|
||||
|
||||
This option adds a `ResponseFormat` property of type `ChatResponseFormat` to `AgentRunOptions`. Agents that support SO check for the presence of
|
||||
this property in the options passed to `RunAsync` to determine whether structured output is requested. If present, they use the schema from `ResponseFormat`
|
||||
to invoke the underlying API and obtain the SO response.
|
||||
|
||||
```csharp
|
||||
public class AgentRunOptions
|
||||
{
|
||||
public ChatResponseFormat? ResponseFormat { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, a generic `RunAsync<T>` method is added to `AIAgent` that initializes the `ResponseFormat` based on the type `T` and delegates to the non-generic `RunAsync`.
|
||||
|
||||
```csharp
|
||||
public abstract class AIAgent
|
||||
{
|
||||
public async Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
var responseFormat = ChatResponseFormat.ForJsonSchema<T>(serializerOptions);
|
||||
|
||||
options = options?.Clone() ?? new AgentRunOptions();
|
||||
options.ResponseFormat = responseFormat;
|
||||
|
||||
AgentResponse response = await this.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new AgentResponse<T>(response, serializerOptions);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Users call the generic `RunAsync<T>` method directly on the agent:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
```
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- The `AIAgent.RunAsync<T>` method is easily discoverable.
|
||||
- No changes required to `AIAgent` decorators
|
||||
|
||||
Cons:
|
||||
- Agents without native SO support will still expose `RunAsync<T>`, which may be misleading.
|
||||
- `ChatClientAgent` exposing `RunAsync<T>` may be misleading when the underlying chat client does not support SO.
|
||||
|
||||
### Decision Table
|
||||
|
||||
| | Option 1: Instance method + RunCoreAsync<T> | Option 2: Extension method + feature collection | Option 3: ITypedAIAgent Interface | Option 4: Instance method + AgentRunOptions.ResponseFormat |
|
||||
|---|---|---|---|---|
|
||||
| Discoverability | ✅ `RunAsync<T>` easily discoverable | ✅ `RunAsync<T>` easily discoverable | ❌ Requires type check and cast | ✅ `RunAsync<T>` easily discoverable |
|
||||
| Decorator changes | ❌ All decorators must override `RunCoreAsync<T>` | ✅ No changes required | ❌ All decorators must implement `ITypedAIAgent` | ✅ No changes required to decorators |
|
||||
| Primitives/collections handling | ✅ Native support via `IChatClient.GetResponseAsync<T>` | ❌ Must wrap/unwrap internally | ✅ Native support via `IChatClient.GetResponseAsync<T>` | ❌ Must wrap/unwrap internally |
|
||||
| Misleading API exposure | ❌ Agents without SO still expose `RunAsync<T>` | ❌ Agents without SO still expose `RunAsync<T>` | ❌ Interface on `ChatClientAgent` may be misleading | ❌ Agents without SO still expose `RunAsync<T>` |
|
||||
| Implementation burden | ❌ Decorators must override method | ❌ Must handle schema wrapping | ❌ Agents must implement all interface members | ✅ Delegates to existing `RunAsync` via `ResponseFormat` |
|
||||
|
||||
## Cross-Cutting Aspects
|
||||
|
||||
1. **The `useJsonSchemaResponseFormat` parameter**: The `ChatClientAgent.RunAsync<T>` method has this parameter to enable structured output on LLMs that do not natively support it.
|
||||
It works by adding a user message like "Respond with a JSON value conforming to the following schema:" along with the JSON schema. However, this approach has not been reliable historically. The recommendation is not to carry this parameter forward, regardless of which option is chosen.
|
||||
|
||||
2. **Primitives and array types handling**: There are a few options for how primitive and array types can be handled in the Agent Framework:
|
||||
|
||||
1. **Never wrap**, regardless of whether the schema is provided via `ResponseFormat` or `RunAsync<T>`.
|
||||
- Pro: No changes needed; user has full control.
|
||||
- Pro: No issues with unwrapping in streaming scenarios.
|
||||
- Con: User must wrap manually.
|
||||
|
||||
2. **Always wrap**, regardless of whether the schema is provided via `ResponseFormat` or `RunAsync<T>`.
|
||||
- Pro: Consistent wrapping behavior; no manual wrapping needed.
|
||||
- Con: Inconsistent unwrapping behavior; it may be unexpected to have SO result wrapped when schema is provided via `ResponseFormat`.
|
||||
- Con: Impossible to know if SO result is wrapped to unwrap it in streaming scenarios.
|
||||
|
||||
3. **Wrap only for `RunAsync<T>`** and do not wrap the schema provided via `ResponseFormat`.
|
||||
- Pro: No unexpectedly wrapped result when schema is provided via `ResponseFormat`.
|
||||
- Pro: Solves the problem with unwrapping in streaming scenarios.
|
||||
|
||||
4. **User decides** whether to wrap schema provided via `ResponseFormat` using a new `wrapPrimitivesAndArrays` property of `ChatResponseFormatJson`. For SO provided via `RunAsync<T>`, AF always wraps.
|
||||
- Pro: No manual wrapping needed; just flip a switch.
|
||||
- Pro: Solves the problem with unwrapping in streaming scenarios.
|
||||
- Con: Extends the public API surface.
|
||||
|
||||
3. **Structured output for agents without native SO support**: Some AI agents in AF do not support structured output natively. This is either because it is not part of the protocol (e.g., A2A agent) or because the agents use LLMs without structured output capabilities.
|
||||
To address this gap, AF can provide the `StructuredOutputAgent` decorator. This decorator wraps any `AIAgent` and adds structured output support by obtaining the text response from the decorated agent and delegating it to a configured chat client for JSON transformation.
|
||||
|
||||
```csharp
|
||||
public class StructuredOutputAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
|
||||
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._chatClient = Throw.IfNull(chatClient);
|
||||
}
|
||||
|
||||
protected override async Task<AgentResponse<T>> RunCoreAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Run the inner agent first, to get back the text response we want to convert.
|
||||
var textResponse = await this.InnerAgent.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Invoke the chat client to transform the text output into structured data.
|
||||
ChatResponse<T> soResponse = await this._chatClient.GetResponseAsync<T>(
|
||||
messages:
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are a json expert and when provided with any text, will convert it to the requested json format."),
|
||||
new ChatMessage(ChatRole.User, textResponse.Text)
|
||||
],
|
||||
serializerOptions: serializerOptions ?? AgentJsonUtilities.DefaultOptions,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new StructuredOutputAgentResponse(soResponse, textResponse);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The decorator preserves the original response from the decorated agent and surfaces it via the `OriginalResponse` property on the returned `StructuredOutputAgentResponse`.
|
||||
This allows users to access both the original unstructured response and the new structured response when using this decorator.
|
||||
```csharp
|
||||
public class StructuredOutputAgentResponse : AgentResponse
|
||||
{
|
||||
internal StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
|
||||
{
|
||||
this.OriginalResponse = agentResponse;
|
||||
}
|
||||
|
||||
public AgentResponse OriginalResponse { get; }
|
||||
}
|
||||
```
|
||||
|
||||
The decorator can be registered during the agent configuration step using the `UseStructuredOutput` extension method on `AIAgentBuilder`.
|
||||
|
||||
```csharp
|
||||
IChatClient meaiChatClient = chatClient.AsIChatClient();
|
||||
|
||||
AIAgent baseAgent = meaiChatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
// Register the StructuredOutputAgent decorator during agent building
|
||||
AIAgent agent = baseAgent
|
||||
.AsBuilder()
|
||||
.UseStructuredOutput(meaiChatClient)
|
||||
.Build();
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
|
||||
Console.WriteLine($"Name: {response.Result.Name}");
|
||||
Console.WriteLine($"Age: {response.Result.Age}");
|
||||
Console.WriteLine($"Occupation: {response.Result.Occupation}");
|
||||
|
||||
var originalResponse = ((StructuredOutputAgentResponse)response.RawRepresentation!).OriginalResponse;
|
||||
Console.WriteLine($"Original unstructured response: {originalResponse.Text}");
|
||||
|
||||
```
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
It was decided to keep both approaches for structured output - via `ResponseFormat` and via `RunAsync<T>` since they serve different scenarios and use cases.
|
||||
|
||||
For the `RunAsync<T>` approach, option 4 was selected, which adds a generic `RunAsync<T>` method to `AIAgent` that works via the new `AgentRunOptions.ResponseFormat` property.
|
||||
This was chosen for its simplicity and because no changes are required to existing `AIAgent` decorators.
|
||||
|
||||
For cross-cutting aspects, the `useJsonSchemaResponseFormat` parameter will not be carried forward due to reliability issues.
|
||||
|
||||
For handling primitives and array types, option 3 was selected: wrap only for `RunAsync<T>` and do not wrap the schema provided via `ResponseFormat`.
|
||||
This avoids the issues described in the Approach 1 section note.
|
||||
|
||||
Finally, it was decided not to include the `StructuredOutputAgent` decorator in the framework, since the reliability of producing structured output via an additional
|
||||
LLM call may not be sufficient for all scenarios. Instead, this pattern is provided as a sample to demonstrate how structured output can be achieved for agents without native support,
|
||||
giving users a reference implementation they can adapt to their own requirements.
|
||||
@@ -0,0 +1,66 @@
|
||||
# AGENTS.md
|
||||
|
||||
Instructions for AI coding agents working in the .NET codebase.
|
||||
|
||||
## Build, Test, and Lint Commands
|
||||
|
||||
```bash
|
||||
# From dotnet/ directory
|
||||
dotnet build # Build all projects
|
||||
dotnet test # Run all tests
|
||||
dotnet format # Auto-fix formatting
|
||||
|
||||
# Build/test a specific project (preferred for isolated changes)
|
||||
dotnet build src/Microsoft.Agents.AI.<Package>
|
||||
dotnet test tests/Microsoft.Agents.AI.<Package>.UnitTests
|
||||
|
||||
# Run a single test
|
||||
dotnet test --filter "FullyQualifiedName~TestClassName.TestMethodName"
|
||||
```
|
||||
|
||||
**Note**: Changes to core packages (`Microsoft.Agents.AI`, `Microsoft.Agents.AI.Abstractions`) affect dependent projects - run checks across the entire solution. For isolated changes, build/test only the affected project to save time.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
dotnet/
|
||||
├── src/
|
||||
│ ├── Microsoft.Agents.AI/ # Core AI agent abstractions
|
||||
│ ├── Microsoft.Agents.AI.Abstractions/ # Shared abstractions and interfaces
|
||||
│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider
|
||||
│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI provider
|
||||
│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider
|
||||
│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration
|
||||
│ └── ... # Other packages
|
||||
├── samples/ # Sample applications
|
||||
└── tests/ # Unit and integration tests
|
||||
```
|
||||
|
||||
### External Dependencies
|
||||
|
||||
The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Abstractions` (external NuGet packages) using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, and `AIContent`.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
|
||||
- **XML docs**: Required for all public methods and classes
|
||||
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
|
||||
- **Private classes**: Should be `sealed` unless subclassed
|
||||
- **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming
|
||||
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking
|
||||
|
||||
## Sample Structure
|
||||
|
||||
1. Copyright header: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
2. Description comment explaining what the sample demonstrates
|
||||
3. Using statements
|
||||
4. Main code logic
|
||||
5. Helper methods at bottom
|
||||
|
||||
Configuration via environment variables (never hardcode secrets). Keep samples simple and focused.
|
||||
|
||||
When adding a new sample:
|
||||
- Create a standalone project in `samples/` with matching directory and project names
|
||||
- Include a README.md explaining what the sample does and how to run it
|
||||
- Add the project to the solution file
|
||||
- Reference the sample in the parent directory's README.md
|
||||
@@ -33,18 +33,18 @@
|
||||
<!-- Newtonsoft.Json -->
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
|
||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.3" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
|
||||
<!-- OpenTelemetry -->
|
||||
@@ -61,9 +61,9 @@
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.2.0-preview.1.26063.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
||||
@@ -71,11 +71,11 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
|
||||
@@ -89,7 +89,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.18" />
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.23" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step07_3rdPartyChatHistoryStorage/Agent_Step07_3rdPartyChatHistoryStorage.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step08_Observability/Agent_Step08_Observability.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
|
||||
@@ -131,6 +131,7 @@
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
|
||||
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
|
||||
@@ -388,6 +389,9 @@
|
||||
<File Path="src/Shared/Throw/README.md" />
|
||||
<File Path="src/Shared/Throw/Throw.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/StructuredOutput/">
|
||||
<File Path="src/Shared/StructuredOutput/StructuredOutputSchemaUtilities.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/tests/">
|
||||
<File Path="tests/.editorconfig" />
|
||||
<File Path="tests/Directory.Build.props" />
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
|
||||
"src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj"
|
||||
]
|
||||
|
||||
@@ -20,4 +20,7 @@
|
||||
<ItemGroup Condition="'$(InjectSharedFoundryAgents)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Foundry\Agents\*.cs" LinkBase="Shared\Foundry" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedStructuredOutput)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StructuredOutput\*.cs" LinkBase="Shared\StructuredOutput" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260128.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260128.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260128.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260212.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260212.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260212.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -42,7 +42,7 @@ public static class Program
|
||||
// Create the Host agent
|
||||
var hostAgent = new HostClientAgent(loggerFactory);
|
||||
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
|
||||
AgentSession session = await hostAgent.Agent!.GetNewSessionAsync(cancellationToken);
|
||||
AgentSession session = await hostAgent.Agent!.CreateSessionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
|
||||
@@ -14,7 +14,10 @@ internal static class HostAgentFactory
|
||||
{
|
||||
internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string assistantId, IList<AITool>? tools = null)
|
||||
{
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
// 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.
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new DefaultAzureCredential());
|
||||
PersistentAgent persistentAgent = await persistentAgentsClient.Administration.GetAgentAsync(assistantId);
|
||||
|
||||
AIAgent agent = await persistentAgentsClient
|
||||
|
||||
@@ -88,7 +88,7 @@ public static class Program
|
||||
description: "AG-UI Client Agent",
|
||||
tools: [changeBackground, readClientClimateSensors]);
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync(cancellationToken);
|
||||
AgentSession session = await agent.CreateSessionAsync(cancellationToken);
|
||||
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
|
||||
try
|
||||
{
|
||||
|
||||
@@ -24,6 +24,9 @@ internal static class ChatClientAgentFactory
|
||||
string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// 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.
|
||||
s_azureOpenAIClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
@@ -78,7 +78,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
@@ -103,4 +103,25 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? result = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (result is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = result;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new In
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent with tools
|
||||
// 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.
|
||||
var agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
@@ -19,6 +19,9 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new In
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent
|
||||
// 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.
|
||||
AzureOpenAIClient azureOpenAIClient = new(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -17,9 +19,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
|
||||
const string JokerName = "Joker";
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public static class FunctionTriggers
|
||||
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
DurableAIAgent writer = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writer.GetNewSessionAsync();
|
||||
AgentSession writerSession = await writer.CreateSessionAsync();
|
||||
|
||||
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
|
||||
message: "Write a concise inspirational sentence about learning.",
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -17,9 +19,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
|
||||
const string WriterName = "WriterAgent";
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -17,9 +19,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Two agents used by the orchestration to demonstrate concurrent execution.
|
||||
const string PhysicistName = "PhysicistAgent";
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Get the spam detection agent
|
||||
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
|
||||
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
|
||||
AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync();
|
||||
|
||||
// Step 1: Check if the email is spam
|
||||
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
|
||||
@@ -43,7 +43,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Generate and send response for legitimate email
|
||||
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
|
||||
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
|
||||
AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync();
|
||||
|
||||
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
|
||||
message:
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -17,9 +19,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Two agents used by the orchestration to demonstrate conditional logic.
|
||||
const string SpamDetectionName = "SpamDetectionAgent";
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentSession writerSession = await writerAgent.CreateSessionAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -17,9 +19,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate human-in-the-loop workflow.
|
||||
const string WriterName = "WriterAgent";
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("Writer");
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentSession writerSession = await writerAgent.CreateSessionAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -21,9 +23,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Agent used by the orchestration to write content.
|
||||
const string WriterAgentName = "Writer";
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// generate a remote MCP endpoint for the app at /runtime/webhooks/mcp with a agent-specific
|
||||
// query tool name.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -23,9 +25,12 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Define three AI agents we are going to use in this application.
|
||||
AIAgent agent1 = client.GetChatClient(deploymentName).AsAIAgent("You are good at telling jokes.", "Joker");
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ public sealed class FunctionTriggers
|
||||
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
|
||||
|
||||
// Create a new agent session
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync(cancellationToken);
|
||||
AgentSession session = await agentProxy.CreateSessionAsync(cancellationToken);
|
||||
string agentSessionId = session.GetService<AgentSessionId>().ToString();
|
||||
|
||||
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
// This pattern is inspired by OpenAI's background mode for the Responses API, which allows clients
|
||||
// to disconnect and reconnect to ongoing agent responses without losing messages.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -38,9 +40,12 @@ int redisStreamTtlMinutes = int.TryParse(
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Travel Planner agent instructions - designed to produce longer responses for demonstrating streaming.
|
||||
const string TravelPlannerName = "TravelPlanner";
|
||||
|
||||
@@ -25,9 +25,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
|
||||
const string JokerName = "Joker";
|
||||
@@ -61,7 +64,7 @@ Console.WriteLine("Enter a message for the Joker agent (or 'exit' to quit):");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create a session for the conversation
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync();
|
||||
AgentSession session = await agentProxy.CreateSessionAsync();
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
||||
@@ -29,9 +29,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
|
||||
const string WriterName = "WriterAgent";
|
||||
@@ -47,7 +50,7 @@ AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstr
|
||||
static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context)
|
||||
{
|
||||
DurableAIAgent writer = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writer.GetNewSessionAsync();
|
||||
AgentSession writerSession = await writer.CreateSessionAsync();
|
||||
|
||||
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
|
||||
message: "Write a concise inspirational sentence about learning.",
|
||||
|
||||
+4
-1
@@ -29,9 +29,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Two agents used by the orchestration to demonstrate concurrent execution.
|
||||
const string PhysicistName = "PhysicistAgent";
|
||||
|
||||
+6
-3
@@ -28,9 +28,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Spam detection agent
|
||||
const string SpamDetectionAgentName = "SpamDetectionAgent";
|
||||
@@ -56,7 +59,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
{
|
||||
// Get the spam detection agent
|
||||
DurableAIAgent spamDetectionAgent = context.GetAgent(SpamDetectionAgentName);
|
||||
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
|
||||
AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync();
|
||||
|
||||
// Step 1: Check if the email is spam
|
||||
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
|
||||
@@ -78,7 +81,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
|
||||
// Generate and send response for legitimate email
|
||||
DurableAIAgent emailAssistantAgent = context.GetAgent(EmailAssistantAgentName);
|
||||
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
|
||||
AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync();
|
||||
|
||||
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
|
||||
message:
|
||||
|
||||
@@ -29,9 +29,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate human-in-the-loop workflow.
|
||||
const string WriterName = "WriterAgent";
|
||||
@@ -48,7 +51,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
{
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentSession writerSession = await writerAgent.CreateSessionAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
|
||||
@@ -30,9 +30,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Agent used by the orchestration to write content.
|
||||
const string WriterAgentName = "Writer";
|
||||
@@ -59,7 +62,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
{
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent(WriterAgentName);
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentSession writerSession = await writerAgent.CreateSessionAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
@@ -299,7 +302,7 @@ Console.WriteLine("Enter a topic for the Publisher agent to write about (or 'exi
|
||||
Console.WriteLine();
|
||||
|
||||
// Create a session for the conversation
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync();
|
||||
AgentSession session = await agentProxy.CreateSessionAsync();
|
||||
|
||||
using CancellationTokenSource cts = new();
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
|
||||
@@ -38,9 +38,12 @@ string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SC
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// 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.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Travel Planner agent instructions - designed to produce longer responses for demonstrating streaming.
|
||||
const string TravelPlannerName = "TravelPlanner";
|
||||
@@ -305,7 +308,7 @@ if (string.IsNullOrWhiteSpace(prompt) || prompt.Equals("exit", StringComparison.
|
||||
}
|
||||
|
||||
// Create a new agent session
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync();
|
||||
AgentSession session = await agentProxy.CreateSessionAsync();
|
||||
AgentSessionId sessionId = session.GetService<AgentSessionId>();
|
||||
string conversationId = sessionId.ToString();
|
||||
|
||||
|
||||
@@ -26,9 +26,12 @@ AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
|
||||
AIAgent a2aAgent = agentCard.AsAIAgent();
|
||||
|
||||
// Create the main agent, and provide the a2a agent skills as a function tools.
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
instructions: "You are a helpful assistant that helps people with travel planning.",
|
||||
|
||||
@@ -16,7 +16,7 @@ AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
|
||||
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
|
||||
AIAgent agent = agentCard.AsAIAgent();
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Start the initial run with a long-running task.
|
||||
AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session);
|
||||
|
||||
@@ -20,7 +20,7 @@ AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent");
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant.")
|
||||
|
||||
@@ -19,6 +19,9 @@ string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent
|
||||
// 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.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
@@ -20,7 +20,7 @@ AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent");
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant.")
|
||||
|
||||
@@ -74,6 +74,9 @@ AITool[] tools =
|
||||
];
|
||||
|
||||
// Create the AI agent with tools
|
||||
// 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.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
@@ -33,7 +33,7 @@ AIAgent agent = chatClient.AsAIAgent(
|
||||
description: "AG-UI Client Agent",
|
||||
tools: frontendTools);
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant.")
|
||||
|
||||
@@ -19,6 +19,9 @@ string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent
|
||||
// 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.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
@@ -52,6 +52,9 @@ AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(Approv
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
// Create base agent
|
||||
// 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.
|
||||
ChatClient openAIChatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
@@ -30,7 +30,7 @@ JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
|
||||
};
|
||||
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful recipe assistant.")
|
||||
|
||||
@@ -29,6 +29,9 @@ string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
|
||||
|
||||
// Create base agent
|
||||
// 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.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
+22
-1
@@ -107,7 +107,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
// Try to deserialize the structured state response
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
// Serialize and emit as STATE_SNAPSHOT via DataContent
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
@@ -134,4 +134,25 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? deserialized = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (deserialized is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = deserialized;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,10 @@ static async Task<string> GetWeatherAsync([Description("The location to get the
|
||||
return $"The weather in {location} is cloudy with a high of 15°C.";
|
||||
}
|
||||
|
||||
using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
// 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.
|
||||
using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
|
||||
.AsBuilder()
|
||||
@@ -128,7 +131,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
|
||||
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
||||
.Build();
|
||||
|
||||
var session = await agent.GetNewSessionAsync();
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id);
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@ string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// 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.
|
||||
using AnthropicClient client = (resource is null)
|
||||
? new AnthropicClient() { ApiKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API
|
||||
: (apiKey is not null)
|
||||
? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication
|
||||
: new AnthropicFoundryClient(new AnthropicFoundryIdentityTokenCredentials(new AzureCliCredential(), resource, ["https://ai.azure.com/.default"])); // Otherwise, use Foundry with Azure TokenCredential authentication
|
||||
: new AnthropicFoundryClient(new AnthropicFoundryIdentityTokenCredentials(new DefaultAzureCredential(), resource, ["https://ai.azure.com/.default"])); // Otherwise, use Foundry with Azure TokenCredential authentication
|
||||
|
||||
AIAgent agent = client.AsAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName);
|
||||
|
||||
|
||||
+5
-2
@@ -13,7 +13,10 @@ const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Get a client to create/retrieve server side agents with.
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
// 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.
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new DefaultAzureCredential());
|
||||
|
||||
// You can create a server side persistent agent with the Azure.AI.Agents.Persistent SDK.
|
||||
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
|
||||
@@ -31,7 +34,7 @@ AIAgent agent2 = await persistentAgentsClient.CreateAIAgentAsync(
|
||||
instructions: JokerInstructions);
|
||||
|
||||
// You can then invoke the agent like any other AIAgent.
|
||||
AgentSession session = await agent1.GetNewSessionAsync();
|
||||
AgentSession session = await agent1.CreateSessionAsync();
|
||||
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
|
||||
|
||||
// Cleanup for sample purposes.
|
||||
|
||||
@@ -13,7 +13,10 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_D
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
|
||||
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new AzureCliCredential());
|
||||
// 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.
|
||||
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Define the agent you want to create. (Prompt Agent in this case)
|
||||
var agentVersionCreationOptions = new AgentVersionCreationOptions(new PromptAgentDefinition(model: deploymentName) { Instructions = "You are good at telling jokes." });
|
||||
@@ -40,7 +43,7 @@ var latestAgentVersion = jokerAgentLatest.GetService<AgentVersion>()!;
|
||||
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
|
||||
|
||||
// Once you have the AIAgent, you can invoke it like any other AIAgent.
|
||||
AgentSession session = await jokerAgentLatest.GetNewSessionAsync();
|
||||
AgentSession session = await jokerAgentLatest.CreateSessionAsync();
|
||||
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", session));
|
||||
|
||||
// This will use the same session to continue the conversation.
|
||||
|
||||
+4
-1
@@ -19,8 +19,11 @@ var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_MODEL_DEPLOYMENT")
|
||||
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
|
||||
|
||||
// Create the OpenAI client with either an API key or Azure CLI credential.
|
||||
// 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.
|
||||
OpenAIClient client = string.IsNullOrWhiteSpace(apiKey)
|
||||
? new OpenAIClient(new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), clientOptions)
|
||||
? new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), clientOptions)
|
||||
: new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
|
||||
|
||||
AIAgent agent = client
|
||||
|
||||
+4
-1
@@ -10,9 +10,12 @@ 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";
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
|
||||
+4
-1
@@ -10,9 +10,12 @@ 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";
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
|
||||
+34
-23
@@ -6,6 +6,7 @@
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using SampleApp;
|
||||
@@ -28,16 +29,28 @@ namespace SampleApp
|
||||
{
|
||||
public override string? Name => "UpperCaseParrotAgent";
|
||||
|
||||
public override ValueTask<AgentSession> GetNewSessionAsync(CancellationToken cancellationToken = default)
|
||||
public readonly ChatHistoryProvider ChatHistoryProvider = new InMemoryChatHistoryProvider();
|
||||
|
||||
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
|
||||
=> new(new CustomAgentSession());
|
||||
|
||||
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> new(new CustomAgentSession(serializedSession, jsonSerializerOptions));
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (session is not CustomAgentSession typedSession)
|
||||
{
|
||||
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
|
||||
}
|
||||
|
||||
return new(JsonSerializer.SerializeToElement(typedSession, jsonSerializerOptions));
|
||||
}
|
||||
|
||||
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> new(serializedState.Deserialize<CustomAgentSession>(jsonSerializerOptions)!);
|
||||
|
||||
protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a session if the user didn't supply one.
|
||||
session ??= await this.GetNewSessionAsync(cancellationToken);
|
||||
session ??= await this.CreateSessionAsync(cancellationToken);
|
||||
|
||||
if (session is not CustomAgentSession typedSession)
|
||||
{
|
||||
@@ -45,18 +58,15 @@ namespace SampleApp
|
||||
}
|
||||
|
||||
// Get existing messages from the store
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(messages);
|
||||
var storeMessages = await typedSession.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken);
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
|
||||
var userAndChatHistoryMessages = await this.ChatHistoryProvider.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 session of the input and output messages.
|
||||
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
await typedSession.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
|
||||
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, userAndChatHistoryMessages, responseMessages);
|
||||
await this.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
|
||||
|
||||
return new AgentResponse
|
||||
{
|
||||
@@ -69,7 +79,7 @@ namespace SampleApp
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a session if the user didn't supply one.
|
||||
session ??= await this.GetNewSessionAsync(cancellationToken);
|
||||
session ??= await this.CreateSessionAsync(cancellationToken);
|
||||
|
||||
if (session is not CustomAgentSession typedSession)
|
||||
{
|
||||
@@ -77,18 +87,15 @@ namespace SampleApp
|
||||
}
|
||||
|
||||
// Get existing messages from the store
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(messages);
|
||||
var storeMessages = await typedSession.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken);
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
|
||||
var userAndChatHistoryMessages = await this.ChatHistoryProvider.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 session of the input and output messages.
|
||||
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
await typedSession.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
|
||||
var invokedContext = new ChatHistoryProvider.InvokedContext(this, session, userAndChatHistoryMessages, responseMessages);
|
||||
await this.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
|
||||
|
||||
foreach (var message in responseMessages)
|
||||
{
|
||||
@@ -130,12 +137,16 @@ namespace SampleApp
|
||||
/// <summary>
|
||||
/// A session type for our custom agent that only supports in memory storage of messages.
|
||||
/// </summary>
|
||||
internal sealed class CustomAgentSession : InMemoryAgentSession
|
||||
internal sealed class CustomAgentSession : AgentSession
|
||||
{
|
||||
internal CustomAgentSession() { }
|
||||
internal CustomAgentSession()
|
||||
{
|
||||
}
|
||||
|
||||
internal CustomAgentSession(JsonElement serializedSessionState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
: base(serializedSessionState, jsonSerializerOptions) { }
|
||||
[JsonConstructor]
|
||||
internal CustomAgentSession(AgentSessionStateBag stateBag) : base(stateBag)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ AIAgent agent2 = await assistantClient.CreateAIAgentAsync(
|
||||
instructions: JokerInstructions);
|
||||
|
||||
// You can invoke the agent like any other AIAgent.
|
||||
AgentSession session = await agent1.GetNewSessionAsync();
|
||||
AgentSession session = await agent1.CreateSessionAsync();
|
||||
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
|
||||
|
||||
// Cleanup for sample purposes.
|
||||
|
||||
+2
-2
@@ -26,11 +26,11 @@ AIAgent agent = new AnthropicClient { ApiKey = apiKey }
|
||||
.AsAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]);
|
||||
|
||||
// Non-streaming agent interaction with function tools.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session));
|
||||
|
||||
// Streaming agent interaction with function tools.
|
||||
session = await agent.GetNewSessionAsync();
|
||||
session = await agent.CreateSessionAsync();
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", session))
|
||||
{
|
||||
Console.WriteLine(update);
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use Anthropic-managed Skills with an AI agent.
|
||||
// Skills are pre-built capabilities provided by Anthropic that can be used with the Claude API.
|
||||
// This sample shows how to:
|
||||
// 1. List available Anthropic-managed skills
|
||||
// 2. Use the pptx skill to create PowerPoint presentations
|
||||
// 3. Download and save generated files
|
||||
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Anthropic.Models.Beta;
|
||||
using Anthropic.Models.Beta.Files;
|
||||
using Anthropic.Models.Beta.Messages;
|
||||
using Anthropic.Models.Beta.Skills;
|
||||
using Anthropic.Services;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
|
||||
// Skills require Claude 4.5 models (Sonnet 4.5, Haiku 4.5, or Opus 4.5)
|
||||
string model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-sonnet-4-5-20250929";
|
||||
|
||||
// Create the Anthropic client
|
||||
AnthropicClient anthropicClient = new() { ApiKey = apiKey };
|
||||
|
||||
// List available Anthropic-managed skills (optional - API may not be available in all regions)
|
||||
Console.WriteLine("Available Anthropic-managed skills:");
|
||||
try
|
||||
{
|
||||
SkillListPage skills = await anthropicClient.Beta.Skills.List(
|
||||
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
|
||||
|
||||
foreach (var skill in skills.Items)
|
||||
{
|
||||
Console.WriteLine($" {skill.Source}: {skill.ID} (version: {skill.LatestVersion})");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" (Skills listing not available: {ex.Message})");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Define the pptx skill - the SDK handles all beta flags and container configuration automatically
|
||||
// when using AsAITool(), so no manual RawRepresentationFactory configuration is needed.
|
||||
BetaSkillParams pptxSkill = new()
|
||||
{
|
||||
Type = BetaSkillParamsType.Anthropic,
|
||||
SkillID = "pptx",
|
||||
Version = "latest"
|
||||
};
|
||||
|
||||
// Create an agent with the pptx skill enabled.
|
||||
// Skills require extended thinking and higher max tokens for complex file generation.
|
||||
// The SDK's AsAITool() handles beta flags and container config automatically.
|
||||
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
|
||||
model: model,
|
||||
instructions: "You are a helpful agent for creating PowerPoint presentations.",
|
||||
tools: [pptxSkill.AsAITool()],
|
||||
clientFactory: (chatClient) => chatClient
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(options =>
|
||||
{
|
||||
options.RawRepresentationFactory = (_) => new MessageCreateParams()
|
||||
{
|
||||
Model = model,
|
||||
MaxTokens = 20000,
|
||||
Messages = [],
|
||||
Thinking = new BetaThinkingConfigParam(
|
||||
new BetaThinkingConfigEnabled(budgetTokens: 10000))
|
||||
};
|
||||
})
|
||||
.Build());
|
||||
|
||||
Console.WriteLine("Creating a presentation about renewable energy...\n");
|
||||
|
||||
// Run the agent with a request to create a presentation
|
||||
AgentResponse response = await agent.RunAsync("Create a simple 3-slide presentation about renewable energy sources. Include a title slide, a slide about solar energy, and a slide about wind energy.");
|
||||
|
||||
Console.WriteLine("#### Agent Response ####");
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
// Display any reasoning/thinking content
|
||||
List<TextReasoningContent> reasoningContents = response.Messages.SelectMany(m => m.Contents.OfType<TextReasoningContent>()).ToList();
|
||||
if (reasoningContents.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n#### Agent Reasoning ####");
|
||||
Console.WriteLine($"\e[92m{string.Join("\n", reasoningContents.Select(c => c.Text))}\e[0m");
|
||||
}
|
||||
|
||||
// Collect generated files from CodeInterpreterToolResultContent outputs
|
||||
List<HostedFileContent> hostedFiles = response.Messages
|
||||
.SelectMany(m => m.Contents.OfType<CodeInterpreterToolResultContent>())
|
||||
.Where(c => c.Outputs is not null)
|
||||
.SelectMany(c => c.Outputs!.OfType<HostedFileContent>())
|
||||
.ToList();
|
||||
|
||||
if (hostedFiles.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n#### Generated Files ####");
|
||||
foreach (HostedFileContent file in hostedFiles)
|
||||
{
|
||||
Console.WriteLine($" FileId: {file.FileId}");
|
||||
|
||||
// Download the file using the Anthropic Files API
|
||||
using HttpResponse fileResponse = await anthropicClient.Beta.Files.Download(
|
||||
file.FileId,
|
||||
new FileDownloadParams { Betas = ["files-api-2025-04-14"] });
|
||||
|
||||
// Save the file to disk
|
||||
string fileName = $"presentation_{file.FileId.Substring(0, 8)}.pptx";
|
||||
using FileStream fileStream = File.Create(fileName);
|
||||
Stream contentStream = await fileResponse.ReadAsStream();
|
||||
await contentStream.CopyToAsync(fileStream);
|
||||
|
||||
Console.WriteLine($" Saved to: {fileName}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("\nToken usage:");
|
||||
Console.WriteLine($"Input: {response.Usage?.InputTokenCount}, Output: {response.Usage?.OutputTokenCount}");
|
||||
if (response.Usage?.AdditionalCounts is not null)
|
||||
{
|
||||
Console.WriteLine($"Additional: {string.Join(", ", response.Usage.AdditionalCounts)}");
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# Using Anthropic Skills with agents
|
||||
|
||||
This sample demonstrates how to use Anthropic-managed Skills with AI agents. Skills are pre-built capabilities provided by Anthropic that can be used with the Claude API.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Listing available Anthropic-managed skills
|
||||
- Creating an AI agent with Anthropic Claude Skills support using the simplified `AsAITool()` approach
|
||||
- Using the pptx skill to create PowerPoint presentations
|
||||
- Downloading and saving generated files to disk
|
||||
- Handling agent responses with generated content
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- Anthropic API key configured
|
||||
- Access to Anthropic Claude models with Skills support
|
||||
|
||||
**Note**: This sample uses Anthropic Claude models with Skills. Skills are a beta feature. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model (e.g., claude-sonnet-4-5-20250929)
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the AgentWithAnthropic sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet\samples\GettingStarted\AgentWithAnthropic
|
||||
dotnet run --project .\Agent_Anthropic_Step04_UsingSkills
|
||||
```
|
||||
|
||||
## Available Anthropic Skills
|
||||
|
||||
Anthropic provides several managed skills that can be used with the Claude API:
|
||||
|
||||
- `pptx` - Create PowerPoint presentations
|
||||
- `xlsx` - Create Excel spreadsheets
|
||||
- `docx` - Create Word documents
|
||||
- `pdf` - Create and analyze PDF documents
|
||||
|
||||
You can list available skills using the Anthropic SDK:
|
||||
|
||||
```csharp
|
||||
SkillListPage skills = await anthropicClient.Beta.Skills.List(
|
||||
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
|
||||
|
||||
foreach (var skill in skills.Items)
|
||||
{
|
||||
Console.WriteLine($"{skill.Source}: {skill.ID} (version: {skill.LatestVersion})");
|
||||
}
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
The sample will:
|
||||
|
||||
1. List all available Anthropic-managed skills
|
||||
2. Create an agent with the pptx skill enabled
|
||||
3. Run the agent with a request to create a presentation
|
||||
4. Display the agent's response text
|
||||
5. Download any generated files and save them to disk
|
||||
6. Display token usage statistics
|
||||
|
||||
## Code highlights
|
||||
|
||||
### Simplified skill configuration
|
||||
|
||||
The Anthropic SDK handles all beta flags and container configuration automatically when using `AsAITool()`:
|
||||
|
||||
```csharp
|
||||
// Define the pptx skill
|
||||
BetaSkillParams pptxSkill = new()
|
||||
{
|
||||
Type = BetaSkillParamsType.Anthropic,
|
||||
SkillID = "pptx",
|
||||
Version = "latest"
|
||||
};
|
||||
|
||||
// Create an agent - the SDK handles beta flags automatically!
|
||||
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
|
||||
model: model,
|
||||
instructions: "You are a helpful agent for creating PowerPoint presentations.",
|
||||
tools: [pptxSkill.AsAITool()]);
|
||||
```
|
||||
|
||||
**Note**: No manual `RawRepresentationFactory`, `Betas`, or `Container` configuration is needed. The SDK automatically adds the required beta headers (`skills-2025-10-02`, `code-execution-2025-08-25`) and configures the container with the skill.
|
||||
|
||||
### Handling generated files
|
||||
|
||||
Generated files are returned as `HostedFileContent` within `CodeInterpreterToolResultContent`:
|
||||
|
||||
```csharp
|
||||
// Collect generated files from response
|
||||
List<HostedFileContent> hostedFiles = response.Messages
|
||||
.SelectMany(m => m.Contents.OfType<CodeInterpreterToolResultContent>())
|
||||
.Where(c => c.Outputs is not null)
|
||||
.SelectMany(c => c.Outputs!.OfType<HostedFileContent>())
|
||||
.ToList();
|
||||
|
||||
// Download and save each file
|
||||
foreach (HostedFileContent file in hostedFiles)
|
||||
{
|
||||
using HttpResponse fileResponse = await anthropicClient.Beta.Files.Download(
|
||||
file.FileId,
|
||||
new FileDownloadParams { Betas = ["files-api-2025-04-14"] });
|
||||
|
||||
string fileName = $"presentation_{file.FileId.Substring(0, 8)}.pptx";
|
||||
await using FileStream fileStream = File.Create(fileName);
|
||||
Stream contentStream = await fileResponse.ReadAsStream();
|
||||
await contentStream.CopyToAsync(fileStream);
|
||||
}
|
||||
```
|
||||
@@ -29,6 +29,7 @@ To use Anthropic with Azure Foundry, you can check the sample [AgentProviders/Ag
|
||||
|[Running a simple agent](./Agent_Anthropic_Step01_Running/)|This sample demonstrates how to create and run a basic agent with Anthropic Claude|
|
||||
|[Using reasoning with an agent](./Agent_Anthropic_Step02_Reasoning/)|This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents|
|
||||
|[Using function tools with an agent](./Agent_Anthropic_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with an Anthropic Claude agent|
|
||||
|[Using Skills with an agent](./Agent_Anthropic_Step04_UsingSkills/)|This sample demonstrates how to use Anthropic-managed Skills (e.g., pptx) with an Anthropic Claude agent|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
+19
-11
@@ -20,7 +20,10 @@ var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_E
|
||||
// Replace this with a vector store implementation of your choice that can persist the chat history long term.
|
||||
VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions()
|
||||
{
|
||||
EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
// 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.
|
||||
EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetEmbeddingClient(embeddingDeploymentName)
|
||||
.AsIEmbeddingGenerator()
|
||||
});
|
||||
@@ -28,33 +31,38 @@ VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions
|
||||
// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in the vector store.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new ChatHistoryMemoryProvider(
|
||||
AIContextProviders = [new ChatHistoryMemoryProvider(
|
||||
vectorStore,
|
||||
collectionName: "chathistory",
|
||||
vectorDimensions: 3072,
|
||||
// Configure the scope values under which chat messages will be stored.
|
||||
// In this case, we are using a fixed user ID and a unique session ID for each new session.
|
||||
storageScope: new() { UserId = "UID1", SessionId = Guid.NewGuid().ToString() },
|
||||
// Configure the scope which would be used to search for relevant prior messages.
|
||||
// In this case, we are searching for any messages for the user across all sessions.
|
||||
searchScope: new() { UserId = "UID1" }))
|
||||
// Callback to configure the initial state of the ChatHistoryMemoryProvider.
|
||||
// The ChatHistoryMemoryProvider stores its state in the AgentSession and this callback
|
||||
// will be called whenever the ChatHistoryMemoryProvider cannot find existing state in the session,
|
||||
// typically the first time it is used with a new session.
|
||||
session => new ChatHistoryMemoryProvider.State(
|
||||
// Configure the scope values under which chat messages will be stored.
|
||||
// In this case, we are using a fixed user ID and a unique session ID for each new session.
|
||||
storageScope: new() { UserId = "UID1", SessionId = Guid.NewGuid().ToString() },
|
||||
// Configure the scope which would be used to search for relevant prior messages.
|
||||
// In this case, we are searching for any messages for the user across all sessions.
|
||||
searchScope: new() { UserId = "UID1" }))]
|
||||
});
|
||||
|
||||
// Start a new session for the agent conversation.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the session that stores conversation history in the vector store.
|
||||
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session));
|
||||
|
||||
// Start a second session. Since we configured the search scope to be across all sessions for the user,
|
||||
// the agent should remember that the user likes pirate jokes.
|
||||
AgentSession? session2 = await agent.GetNewSessionAsync();
|
||||
AgentSession? session2 = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the second session.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2));
|
||||
|
||||
+17
-13
@@ -24,27 +24,31 @@ using HttpClient mem0HttpClient = new();
|
||||
mem0HttpClient.BaseAddress = new Uri(mem0ServiceUri);
|
||||
mem0HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", mem0ApiKey);
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
|
||||
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
|
||||
// If each session should have its own Mem0 scope, you can create a new id per session here:
|
||||
// ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
|
||||
// In this case we are storing memories scoped by application and user instead so that memories are retained across threads.
|
||||
? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" })
|
||||
// For cases where we are restoring from serialized state:
|
||||
: new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions))
|
||||
// The stateInitializer can be used to customize the Mem0 scope per session and it will be called each time a session
|
||||
// is encountered by the Mem0Provider that does not already have Mem0Provider state stored on the session.
|
||||
// If each session should have its own Mem0 scope, you can create a new id per session via the stateInitializer, e.g.:
|
||||
// new Mem0Provider(mem0HttpClient, stateInitializer: _ => new(new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() }))
|
||||
// In our case we are storing memories scoped by application and user instead so that memories are retained across threads.
|
||||
AIContextProviders = [new Mem0Provider(mem0HttpClient, stateInitializer: _ => new(new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" }))]
|
||||
});
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Clear any existing memories for this scope to demonstrate fresh behavior.
|
||||
Mem0Provider mem0Provider = session.GetService<Mem0Provider>()!;
|
||||
await mem0Provider.ClearStoredMemoriesAsync();
|
||||
// Note that the ClearStoredMemoriesAsync method will clear memories
|
||||
// using the scope stored in the session, or provided via the stateInitializer.
|
||||
Mem0Provider mem0Provider = agent.GetService<Mem0Provider>()!;
|
||||
await mem0Provider.ClearStoredMemoriesAsync(session);
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
|
||||
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
|
||||
@@ -55,10 +59,10 @@ await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
|
||||
|
||||
Console.WriteLine("\n>> Serialize and deserialize the session to demonstrate persisted state\n");
|
||||
JsonElement serializedSession = session.Serialize();
|
||||
JsonElement serializedSession = await agent.SerializeSessionAsync(session);
|
||||
AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSession);
|
||||
Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
|
||||
|
||||
Console.WriteLine("\n>> Start a new session that shares the same Mem0 scope\n");
|
||||
AgentSession newSession = await agent.GetNewSessionAsync();
|
||||
AgentSession newSession = await agent.CreateSessionAsync();
|
||||
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
|
||||
|
||||
+47
-38
@@ -18,9 +18,12 @@ using SampleApp;
|
||||
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";
|
||||
|
||||
// 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.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
// Create the agent and provide a factory to add our custom memory component to
|
||||
@@ -33,11 +36,11 @@ ChatClient chatClient = new AzureOpenAIClient(
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
|
||||
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions))
|
||||
AIContextProviders = [new UserInfoMemory(chatClient.AsIChatClient())]
|
||||
});
|
||||
|
||||
// Create a new session for the conversation.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
Console.WriteLine(">> Use session with blank memory\n");
|
||||
|
||||
@@ -47,7 +50,7 @@ Console.WriteLine(await agent.RunAsync("My name is RuaidhrĂ", session));
|
||||
Console.WriteLine(await agent.RunAsync("I am 20 years old", session));
|
||||
|
||||
// We can serialize the session. The serialized state will include the state of the memory component.
|
||||
var sesionElement = session.Serialize();
|
||||
JsonElement sesionElement = await agent.SerializeSessionAsync(session);
|
||||
|
||||
Console.WriteLine("\n>> Use deserialized session with previously created memories\n");
|
||||
|
||||
@@ -55,10 +58,10 @@ Console.WriteLine("\n>> Use deserialized session with previously created memorie
|
||||
var deserializedSession = await agent.DeserializeSessionAsync(sesionElement);
|
||||
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedSession));
|
||||
|
||||
Console.WriteLine("\n>> Read memories from memory component\n");
|
||||
Console.WriteLine("\n>> Read memories using memory component\n");
|
||||
|
||||
// It's possible to access the memory component via the session's GetService method.
|
||||
var userInfo = deserializedSession.GetService<UserInfoMemory>()?.UserInfo;
|
||||
// It's possible to access the memory component via the agent's GetService method.
|
||||
var userInfo = agent.GetService<UserInfoMemory>()?.GetUserInfo(deserializedSession);
|
||||
|
||||
// Output the user info that was captured by the memory component.
|
||||
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
|
||||
@@ -66,12 +69,12 @@ Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");
|
||||
|
||||
Console.WriteLine("\n>> Use new session with previously created memories\n");
|
||||
|
||||
// It is also possible to set the memories in a memory component on an individual session.
|
||||
// It is also possible to set the memories using a memory component on an individual session.
|
||||
// This is useful if we want to start a new session, but have it share the same memories as a previous session.
|
||||
var newSession = await agent.GetNewSessionAsync();
|
||||
if (userInfo is not null && newSession.GetService<UserInfoMemory>() is UserInfoMemory newSessionMemory)
|
||||
var newSession = await agent.CreateSessionAsync();
|
||||
if (userInfo is not null && agent.GetService<UserInfoMemory>() is UserInfoMemory newSessionMemory)
|
||||
{
|
||||
newSessionMemory.UserInfo = userInfo;
|
||||
newSessionMemory.SetUserInfo(newSession, userInfo);
|
||||
}
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
@@ -86,28 +89,27 @@ namespace SampleApp
|
||||
internal sealed class UserInfoMemory : AIContextProvider
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
private readonly Func<AgentSession?, UserInfo> _stateInitializer;
|
||||
|
||||
public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
|
||||
public UserInfoMemory(IChatClient chatClient, Func<AgentSession?, UserInfo>? stateInitializer = null)
|
||||
{
|
||||
this._chatClient = chatClient;
|
||||
this.UserInfo = userInfo ?? new UserInfo();
|
||||
this._stateInitializer = stateInitializer ?? (_ => new UserInfo());
|
||||
}
|
||||
|
||||
public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public UserInfo GetUserInfo(AgentSession session)
|
||||
=> session.StateBag.GetValue<UserInfo>(nameof(UserInfoMemory)) ?? new UserInfo();
|
||||
|
||||
public void SetUserInfo(AgentSession session, UserInfo userInfo)
|
||||
=> session.StateBag.SetValue(nameof(UserInfoMemory), userInfo);
|
||||
|
||||
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._chatClient = chatClient;
|
||||
var userInfo = context.Session?.StateBag.GetValue<UserInfo>(nameof(UserInfoMemory))
|
||||
?? this._stateInitializer.Invoke(context.Session);
|
||||
|
||||
this.UserInfo = serializedState.ValueKind == JsonValueKind.Object ?
|
||||
serializedState.Deserialize<UserInfo>(jsonSerializerOptions)! :
|
||||
new UserInfo();
|
||||
}
|
||||
|
||||
public UserInfo UserInfo { get; set; }
|
||||
|
||||
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
|
||||
if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
|
||||
if ((userInfo.UserName is null || userInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
|
||||
{
|
||||
var result = await this._chatClient.GetResponseAsync<UserInfo>(
|
||||
context.RequestMessages,
|
||||
@@ -117,36 +119,43 @@ namespace SampleApp
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
this.UserInfo.UserName ??= result.Result.UserName;
|
||||
this.UserInfo.UserAge ??= result.Result.UserAge;
|
||||
userInfo.UserName ??= result.Result.UserName;
|
||||
userInfo.UserAge ??= result.Result.UserAge;
|
||||
}
|
||||
|
||||
context.Session?.StateBag.SetValue(nameof(UserInfoMemory), userInfo);
|
||||
}
|
||||
|
||||
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputContext = context.AIContext;
|
||||
var userInfo = context.Session?.StateBag.GetValue<UserInfo>(nameof(UserInfoMemory))
|
||||
?? this._stateInitializer.Invoke(context.Session);
|
||||
|
||||
StringBuilder instructions = new();
|
||||
if (!string.IsNullOrEmpty(inputContext.Instructions))
|
||||
{
|
||||
instructions.AppendLine(inputContext.Instructions);
|
||||
}
|
||||
|
||||
// If we don't already know the user's name and age, add instructions to ask for them, otherwise just provide what we have to the context.
|
||||
instructions
|
||||
.AppendLine(
|
||||
this.UserInfo.UserName is null ?
|
||||
userInfo.UserName is null ?
|
||||
"Ask the user for their name and politely decline to answer any questions until they provide it." :
|
||||
$"The user's name is {this.UserInfo.UserName}.")
|
||||
$"The user's name is {userInfo.UserName}.")
|
||||
.AppendLine(
|
||||
this.UserInfo.UserAge is null ?
|
||||
userInfo.UserAge is null ?
|
||||
"Ask the user for their age and politely decline to answer any questions until they provide it." :
|
||||
$"The user's age is {this.UserInfo.UserAge}.");
|
||||
$"The user's age is {userInfo.UserAge}.");
|
||||
|
||||
return new ValueTask<AIContext>(new AIContext
|
||||
{
|
||||
Instructions = instructions.ToString()
|
||||
Instructions = instructions.ToString(),
|
||||
Messages = inputContext.Messages,
|
||||
Tools = inputContext.Tools
|
||||
});
|
||||
}
|
||||
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
return JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UserInfo
|
||||
|
||||
+3
-8
@@ -5,7 +5,6 @@
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5";
|
||||
@@ -15,14 +14,10 @@ var client = new OpenAIClient(apiKey)
|
||||
.AsIChatClient().AsBuilder()
|
||||
.ConfigureOptions(o =>
|
||||
{
|
||||
o.RawRepresentationFactory = _ => new CreateResponseOptions()
|
||||
o.Reasoning = new()
|
||||
{
|
||||
ReasoningOptions = new()
|
||||
{
|
||||
ReasoningEffortLevel = ResponseReasoningEffortLevel.Medium,
|
||||
// Verbosity requires OpenAI verified Organization
|
||||
ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Detailed
|
||||
}
|
||||
Effort = ReasoningEffort.Medium,
|
||||
Output = ReasoningOutput.Full,
|
||||
};
|
||||
}).Build();
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createCon
|
||||
string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!;
|
||||
|
||||
// Create a session for the conversation - this enables conversation state management for subsequent turns
|
||||
AgentSession session = await agent.GetNewSessionAsync(conversationId);
|
||||
AgentSession session = await agent.CreateSessionAsync(conversationId);
|
||||
|
||||
Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
|
||||
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ The `AgentSession` works with `ChatClientAgentRunOptions` to link the agent to a
|
||||
ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } };
|
||||
|
||||
// Create a session for the conversation
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// First call links the session to the conversation
|
||||
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], session, agentRunOptions);
|
||||
@@ -59,7 +59,7 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
|
||||
1. **Create an OpenAI Client**: Initialize an `OpenAIClient` with your API key
|
||||
2. **Create a Conversation**: Use `ConversationClient` to create a server-side conversation
|
||||
3. **Create an Agent**: Initialize an `OpenAIResponseClientAgent` with the desired model and instructions
|
||||
4. **Create a Session**: Call `agent.GetNewSessionAsync()` to create a new conversation session
|
||||
4. **Create a Session**: Call `agent.CreateSessionAsync()` to create a new conversation session
|
||||
5. **Link Session to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call
|
||||
6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the session - context is maintained
|
||||
7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()`
|
||||
|
||||
+13
-6
@@ -18,9 +18,12 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? th
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
||||
|
||||
// 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.
|
||||
AzureOpenAIClient azureOpenAIClient = new(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential());
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// Create an In-Memory vector store that uses the Azure OpenAI embedding model to generate embeddings.
|
||||
VectorStore vectorStore = new InMemoryVectorStore(new()
|
||||
@@ -62,15 +65,19 @@ AIAgent agent = azureOpenAIClient
|
||||
.AsAIAgent(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, ct) => new ValueTask<AIContextProvider>(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
|
||||
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
|
||||
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
|
||||
// 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.
|
||||
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(new InMemoryChatHistoryProvider(ctx.SerializedState, ctx.JsonSerializerOptions)
|
||||
.WithAIContextProviderMessageRemoval()),
|
||||
// By default the chat history provider will store all messages, except for those that came from chat history in the first place.
|
||||
// We also want to maintain that exclusion here.
|
||||
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
|
||||
}),
|
||||
});
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
Console.WriteLine(">> Asking about returns\n");
|
||||
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session));
|
||||
|
||||
+6
-3
@@ -19,9 +19,12 @@ var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_E
|
||||
var afOverviewUrl = "https://github.com/MicrosoftDocs/semantic-kernel-docs/blob/main/agent-framework/overview/agent-framework-overview.md";
|
||||
var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md";
|
||||
|
||||
// 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.
|
||||
AzureOpenAIClient azureOpenAIClient = new(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential());
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// Create a Qdrant vector store that uses the Azure OpenAI embedding model to generate embeddings.
|
||||
QdrantClient client = new("localhost");
|
||||
@@ -71,10 +74,10 @@ AIAgent agent = azureOpenAIClient
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
|
||||
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions))
|
||||
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)]
|
||||
});
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
Console.WriteLine(">> Asking about SK sessions\n");
|
||||
Console.WriteLine(await agent.RunAsync("Hi! How do I create a thread/session in Semantic Kernel?", session));
|
||||
|
||||
+6
-3
@@ -22,17 +22,20 @@ TextSearchProviderOptions textSearchOptions = new()
|
||||
RecentMessageMemoryLimit = 6,
|
||||
};
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(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, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions))
|
||||
AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)]
|
||||
});
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
Console.WriteLine(">> Asking about returns\n");
|
||||
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session));
|
||||
|
||||
+5
-2
@@ -15,9 +15,12 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOIN
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// Create an AI Project client and get an OpenAI client that works with the foundry service.
|
||||
// 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 AzureCliCredential());
|
||||
new DefaultAzureCredential());
|
||||
OpenAIClient openAIClient = aiProjectClient.GetProjectOpenAIClient();
|
||||
|
||||
// Upload the file that contains the data to be used for RAG to the Foundry service.
|
||||
@@ -43,7 +46,7 @@ AIAgent agent = await aiProjectClient
|
||||
instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
|
||||
tools: [fileSearchTool]);
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
Console.WriteLine(">> Asking about returns\n");
|
||||
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session));
|
||||
|
||||
@@ -10,9 +10,12 @@ 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";
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
|
||||
@@ -10,19 +10,22 @@ 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";
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent with a multi-turn conversation, where the context is preserved in the session object.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
|
||||
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
|
||||
|
||||
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the session object.
|
||||
session = await agent.GetNewSessionAsync();
|
||||
session = await agent.CreateSessionAsync();
|
||||
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.", session))
|
||||
{
|
||||
Console.WriteLine(update);
|
||||
|
||||
@@ -18,9 +18,12 @@ static string GetWeather([Description("The location to get the weather for.")] s
|
||||
=> $"The weather in {location} is cloudy with a high of 15°C.";
|
||||
|
||||
// Create the chat client and agent, and provide the function tool to the agent.
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
|
||||
|
||||
|
||||
+16
-15
@@ -23,42 +23,43 @@ static string GetWeather([Description("The location to get the weather for.")] s
|
||||
|
||||
// Create the chat client and agent.
|
||||
// Note that we are wrapping the function tool with ApprovalRequiredAIFunction to require user approval before invoking it.
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
|
||||
|
||||
// Call the agent and check if there are any user input requests to handle.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
var response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
|
||||
var userInputRequests = response.UserInputRequests.ToList();
|
||||
// Call the agent and check if there are any function approval requests to handle.
|
||||
// For simplicity, we are assuming here that only function approvals are pending.
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
|
||||
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
|
||||
|
||||
// For streaming use:
|
||||
// var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", session).ToListAsync();
|
||||
// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
|
||||
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<FunctionApprovalRequestContent>().ToList();
|
||||
|
||||
while (userInputRequests.Count > 0)
|
||||
while (approvalRequests.Count > 0)
|
||||
{
|
||||
// Ask the user to approve each function call request.
|
||||
// For simplicity, we are assuming here that only function approval requests are being made.
|
||||
var userInputResponses = userInputRequests
|
||||
.OfType<FunctionApprovalRequestContent>()
|
||||
.Select(functionApprovalRequest =>
|
||||
List<ChatMessage> userInputResponses = approvalRequests
|
||||
.ConvertAll(functionApprovalRequest =>
|
||||
{
|
||||
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
|
||||
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
|
||||
})
|
||||
.ToList();
|
||||
});
|
||||
|
||||
// Pass the user input responses back to the agent for further processing.
|
||||
response = await agent.RunAsync(userInputResponses, session);
|
||||
|
||||
userInputRequests = response.UserInputRequests.ToList();
|
||||
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
|
||||
|
||||
// For streaming use:
|
||||
// updates = await agent.RunStreamingAsync(userInputResponses, session).ToListAsync();
|
||||
// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
|
||||
// approvalRequests = updates.SelectMany(x => x.Contents).OfType<FunctionApprovalRequestContent>().ToList();
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nAgent: {response}");
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace SampleApp;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for adding structured output capabilities to <see cref="AIAgentBuilder"/> instances.
|
||||
/// </summary>
|
||||
internal static class AIAgentBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds structured output capabilities to the agent pipeline, enabling conversion of text responses to structured JSON format.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="AIAgentBuilder"/> to which structured output support will be added.</param>
|
||||
/// <param name="chatClient">
|
||||
/// The chat client used to transform text responses into structured JSON format.
|
||||
/// If <see langword="null"/>, the chat client will be resolved from the service provider.
|
||||
/// </param>
|
||||
/// <param name="optionsFactory">
|
||||
/// An optional factory function that returns the <see cref="StructuredOutputAgentOptions"/> instance to use.
|
||||
/// This allows for fine-tuning the structured output behavior such as setting the response format or system message.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="AIAgentBuilder"/> with structured output capabilities added, enabling method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A <see cref="ChatResponseFormatJson"/> must be specified either through the
|
||||
/// <see cref="AgentRunOptions.ResponseFormat"/> at runtime or the <see cref="StructuredOutputAgentOptions.ChatOptions"/>
|
||||
/// provided during configuration.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static AIAgentBuilder UseStructuredOutput(
|
||||
this AIAgentBuilder builder,
|
||||
IChatClient? chatClient = null,
|
||||
Func<StructuredOutputAgentOptions>? optionsFactory = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
return builder.Use((innerAgent, services) =>
|
||||
{
|
||||
chatClient ??= services?.GetService<IChatClient>()
|
||||
?? throw new InvalidOperationException($"No {nameof(IChatClient)} was provided and none could be resolved from the service provider. Either provide an {nameof(IChatClient)} explicitly or register one in the dependency injection container.");
|
||||
|
||||
return new StructuredOutputAgent(innerAgent, chatClient, optionsFactory?.Invoke());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,64 +8,176 @@ using System.Text.Json.Serialization;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
using SampleApp;
|
||||
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
|
||||
|
||||
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";
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// Create chat client to be used by chat client agents.
|
||||
// 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.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
// Create the ChatClientAgent with the specified name and instructions.
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
// Demonstrates how to work with structured output via ResponseFormat with the non-generic RunAsync method.
|
||||
// This approach is useful when:
|
||||
// a. Structured output is used for inter-agent communication, where one agent produces structured output
|
||||
// and passes it as text to another agent as input, without the need for the caller to directly work with the structured output.
|
||||
// b. The type of the structured output is not known at compile time, so the generic RunAsync<T> method cannot be used.
|
||||
// c. The type of the structured output is represented by JSON schema only, without a corresponding class or type in the code.
|
||||
await UseStructuredOutputWithResponseFormatAsync(chatClient);
|
||||
|
||||
// Set PersonInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke the agent with some unstructured input.
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
// Demonstrates how to work with structured output via the generic RunAsync<T> method.
|
||||
// This approach is useful when the caller needs to directly work with the structured output in the code
|
||||
// via an instance of the corresponding class or type and the type is known at compile time.
|
||||
await UseStructuredOutputWithRunAsync(chatClient);
|
||||
|
||||
// Access the structured output via the Result property of the agent response.
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {response.Result.Name}");
|
||||
Console.WriteLine($"Age: {response.Result.Age}");
|
||||
Console.WriteLine($"Occupation: {response.Result.Occupation}");
|
||||
// Demonstrates how to work with structured output when streaming using the RunStreamingAsync method.
|
||||
await UseStructuredOutputWithRunStreamingAsync(chatClient);
|
||||
|
||||
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
|
||||
ChatClientAgent agentWithPersonInfo = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
// Demonstrates how to add structured output support to agents that don't natively support it using the structured output middleware.
|
||||
// This approach is useful when working with agents that don't support structured output natively, or agents using models
|
||||
// that don't have the capability to produce structured output, allowing you to still leverage structured output features by transforming
|
||||
// the text output from the agent into structured data using a chat client.
|
||||
await UseStructuredOutputWithMiddlewareAsync(chatClient);
|
||||
|
||||
static async Task UseStructuredOutputWithResponseFormatAsync(ChatClient chatClient)
|
||||
{
|
||||
Name = "HelpfulAssistant",
|
||||
ChatOptions = new() { Instructions = "You are a helpful assistant.", ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
Console.WriteLine("=== Structured Output with ResponseFormat ===");
|
||||
|
||||
// Invoke the agent with some unstructured input while streaming, to extract the structured information from.
|
||||
var updates = agentWithPersonInfo.RunStreamingAsync("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
// Create the agent
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "HelpfulAssistant",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant.",
|
||||
// Specify CityInfo as the type parameter of ForJsonSchema to indicate the expected structured output from the agent.
|
||||
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<CityInfo>()
|
||||
}
|
||||
});
|
||||
|
||||
// Assemble all the parts of the streamed output, since we can only deserialize once we have the full json,
|
||||
// then deserialize the response into the PersonInfo class.
|
||||
PersonInfo personInfo = (await updates.ToAgentResponseAsync()).Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
// Invoke the agent with some unstructured input to extract the structured information from.
|
||||
AgentResponse response = await agent.RunAsync("Provide information about the capital of France.");
|
||||
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
// Access the structured output via the Text property of the agent response as JSON in scenarios when JSON as text is required
|
||||
// and no object instance is needed (e.g., for logging, forwarding to another service, or storing in a database).
|
||||
Console.WriteLine("Assistant Output (JSON):");
|
||||
Console.WriteLine(response.Text);
|
||||
Console.WriteLine();
|
||||
|
||||
// Deserialize the JSON text to work with the structured object in scenarios when you need to access properties,
|
||||
// perform operations, or pass the data to methods that require the typed object instance.
|
||||
CityInfo cityInfo = JsonSerializer.Deserialize<CityInfo>(response.Text)!;
|
||||
|
||||
Console.WriteLine("Assistant Output (Deserialized):");
|
||||
Console.WriteLine($"Name: {cityInfo.Name}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static async Task UseStructuredOutputWithRunAsync(ChatClient chatClient)
|
||||
{
|
||||
Console.WriteLine("=== Structured Output with RunAsync<T> ===");
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
// Set CityInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke it with some unstructured input.
|
||||
AgentResponse<CityInfo> response = await agent.RunAsync<CityInfo>("Provide information about the capital of France.");
|
||||
|
||||
// Access the structured output via the Result property of the agent response.
|
||||
CityInfo cityInfo = response.Result;
|
||||
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {cityInfo.Name}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static async Task UseStructuredOutputWithRunStreamingAsync(ChatClient chatClient)
|
||||
{
|
||||
Console.WriteLine("=== Structured Output with RunStreamingAsync ===");
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "HelpfulAssistant",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant.",
|
||||
// Specify CityInfo as the type parameter of ForJsonSchema to indicate the expected structured output from the agent.
|
||||
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<CityInfo>()
|
||||
}
|
||||
});
|
||||
|
||||
// Invoke the agent with some unstructured input while streaming, to extract the structured information from.
|
||||
IAsyncEnumerable<AgentResponseUpdate> updates = agent.RunStreamingAsync("Provide information about the capital of France.");
|
||||
|
||||
// Assemble all the parts of the streamed output.
|
||||
AgentResponse nonGenericResponse = await updates.ToAgentResponseAsync();
|
||||
|
||||
// Access the structured output by deserializing JSON in the Text property.
|
||||
CityInfo cityInfo = JsonSerializer.Deserialize<CityInfo>(nonGenericResponse.Text)!;
|
||||
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {cityInfo.Name}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static async Task UseStructuredOutputWithMiddlewareAsync(ChatClient chatClient)
|
||||
{
|
||||
Console.WriteLine("=== Structured Output with UseStructuredOutput Middleware ===");
|
||||
|
||||
// Create chat client that will transform the agent text response into structured output.
|
||||
IChatClient meaiChatClient = chatClient.AsIChatClient();
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = meaiChatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
// Add structured output middleware via UseStructuredOutput method to add structured output support to the agent.
|
||||
// This middleware transforms the agent's text response into structured data using a chat client.
|
||||
// Since our agent does support structured output natively, we will add a middleware that removes ResponseFormat
|
||||
// from the AgentRunOptions to emulate an agent that doesn't support structured output natively
|
||||
agent = agent
|
||||
.AsBuilder()
|
||||
.UseStructuredOutput(meaiChatClient)
|
||||
.Use(ResponseFormatRemovalMiddleware, null)
|
||||
.Build();
|
||||
|
||||
// Set CityInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke it with some unstructured input.
|
||||
AgentResponse<CityInfo> response = await agent.RunAsync<CityInfo>("Provide information about the capital of France.");
|
||||
|
||||
// Access the structured output via the Result property of the agent response.
|
||||
CityInfo cityInfo = response.Result;
|
||||
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {cityInfo.Name}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static Task<AgentResponse> ResponseFormatRemovalMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
|
||||
{
|
||||
// Remove any ResponseFormat from the options to emulate an agent that doesn't support structured output natively.
|
||||
options = options?.Clone();
|
||||
options?.ResponseFormat = null;
|
||||
|
||||
return innerAgent.RunAsync(messages, session, options, cancellationToken);
|
||||
}
|
||||
|
||||
namespace SampleApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent.
|
||||
/// Represents information about a city, including its name.
|
||||
/// </summary>
|
||||
[Description("Information about a person including their name, age, and occupation")]
|
||||
public class PersonInfo
|
||||
[Description("Information about a city")]
|
||||
public sealed class CityInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("age")]
|
||||
public int? Age { get; set; }
|
||||
|
||||
[JsonPropertyName("occupation")]
|
||||
public string? Occupation { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Structured Output with ChatClientAgent
|
||||
|
||||
This sample demonstrates how to configure ChatClientAgent to produce structured output in JSON format using various approaches.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- **ResponseFormat approach**: Configuring agents with JSON schema response format via `ChatResponseFormat.ForJsonSchema<T>()` for inter-agent communication or when the type is not known at compile time
|
||||
- **Generic RunAsync<T> method**: Using the generic `RunAsync<T>` method for structured output when the caller needs to work directly with typed objects
|
||||
- **Structured output with Streaming**: Using `RunStreamingAsync` to stream responses while still obtaining structured output by assembling and deserializing the streamed content
|
||||
- **StructuredOutput middleware**: Adding structured output support to agents that don't natively support it (like A2A agents or models without structured output capability) by transforming text output into structured data using a chat client
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource
|
||||
|
||||
**Note**: This sample uses Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
The sample will demonstrate four different approaches to structured output:
|
||||
|
||||
1. **Structured Output with ResponseFormat**: Creates an agent with `ResponseFormat` set to `ForJsonSchema<CityInfo>()`, invokes it with unstructured input, and accesses the structured output via the `Text` property
|
||||
2. **Structured Output with RunAsync<T>**: Creates an agent and uses the generic `RunAsync<CityInfo>()` method to get a typed `AgentResponse<CityInfo>` with the result accessible via the `Result` property
|
||||
3. **Structured Output with RunStreamingAsync**: Creates an agent with JSON schema response format, streams the response using `RunStreamingAsync`, assembles the updates using `ToAgentResponseAsync()`, and deserializes the JSON text into a typed object
|
||||
4. **Structured Output with StructuredOutput Middleware**: Uses the `UseStructuredOutput` method on `AIAgentBuilder` to add structured output support to agents that don't natively support it
|
||||
|
||||
Each approach will output information about the capital of France (Paris) in a structured format.
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace SampleApp;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating AI agent that converts text responses from an inner AI agent into structured output using a chat client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="StructuredOutputAgent"/> wraps an inner agent and uses a chat client to transform
|
||||
/// the inner agent's text response into a structured JSON format based on the specified response format.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This agent requires a <see cref="ChatResponseFormatJson"/> to be specified either through the
|
||||
/// <see cref="AgentRunOptions.ResponseFormat"/> or the <see cref="StructuredOutputAgentOptions.ChatOptions"/>
|
||||
/// provided during construction.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class StructuredOutputAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
private readonly StructuredOutputAgentOptions? _agentOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StructuredOutputAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The underlying agent that generates text responses to be converted to structured output.</param>
|
||||
/// <param name="chatClient">The chat client used to transform text responses into structured JSON format.</param>
|
||||
/// <param name="options">Optional configuration options for the structured output agent.</param>
|
||||
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient, StructuredOutputAgentOptions? options = null)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient));
|
||||
this._agentOptions = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Run the inner agent first, to get back the text response we want to convert.
|
||||
var textResponse = await this.InnerAgent.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Invoke the chat client to transform the text output into structured data.
|
||||
ChatResponse soResponse = await this._chatClient.GetResponseAsync(
|
||||
messages: this.GetChatMessages(textResponse.Text),
|
||||
options: this.GetChatOptions(options),
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new StructuredOutputAgentResponse(soResponse, textResponse);
|
||||
}
|
||||
|
||||
private List<ChatMessage> GetChatMessages(string? textResponseText)
|
||||
{
|
||||
List<ChatMessage> chatMessages = [];
|
||||
|
||||
if (this._agentOptions?.ChatClientSystemMessage is not null)
|
||||
{
|
||||
chatMessages.Add(new ChatMessage(ChatRole.System, this._agentOptions.ChatClientSystemMessage));
|
||||
}
|
||||
|
||||
chatMessages.Add(new ChatMessage(ChatRole.User, textResponseText));
|
||||
|
||||
return chatMessages;
|
||||
}
|
||||
|
||||
private ChatOptions GetChatOptions(AgentRunOptions? options)
|
||||
{
|
||||
ChatResponseFormat responseFormat = options?.ResponseFormat
|
||||
?? this._agentOptions?.ChatOptions?.ResponseFormat
|
||||
?? throw new InvalidOperationException($"A response format of type '{nameof(ChatResponseFormatJson)}' must be specified, but none was specified.");
|
||||
|
||||
if (responseFormat is not ChatResponseFormatJson jsonResponseFormat)
|
||||
{
|
||||
throw new NotSupportedException($"A response format of type '{nameof(ChatResponseFormatJson)}' must be specified, but was '{responseFormat.GetType().Name}'.");
|
||||
}
|
||||
|
||||
var chatOptions = this._agentOptions?.ChatOptions?.Clone() ?? new ChatOptions();
|
||||
chatOptions.ResponseFormat = jsonResponseFormat;
|
||||
return chatOptions;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace SampleApp;
|
||||
|
||||
/// <summary>
|
||||
/// Represents configuration options for a <see cref="StructuredOutputAgent"/>.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1812 // Instantiated via AIAgentBuilderExtensions.UseStructuredOutput optionsFactory parameter
|
||||
internal sealed class StructuredOutputAgentOptions
|
||||
#pragma warning restore CA1812
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the system message to use when invoking the chat client for structured output conversion.
|
||||
/// </summary>
|
||||
public string? ChatClientSystemMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chat options to use for the structured output conversion by the chat client
|
||||
/// used by the agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is optional. The <see cref="ChatOptions.ResponseFormat"/> should be set to a
|
||||
/// <see cref="ChatResponseFormatJson"/> instance to specify the expected JSON schema for the structured output.
|
||||
/// Note that if <see cref="AgentRunOptions.ResponseFormat"/> is provided when running the agent,
|
||||
/// it will take precedence and override the <see cref="ChatOptions.ResponseFormat"/> specified here.
|
||||
/// </remarks>
|
||||
public ChatOptions? ChatOptions { get; set; }
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace SampleApp;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an agent response that contains structured output and
|
||||
/// the original agent response from which the structured output was generated.
|
||||
/// </summary>
|
||||
internal sealed class StructuredOutputAgentResponse : AgentResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StructuredOutputAgentResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatResponse">The <see cref="ChatResponse"/> containing the structured output.</param>
|
||||
/// <param name="agentResponse">The original <see cref="AgentResponse"/> from the inner agent.</param>
|
||||
public StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
|
||||
{
|
||||
this.OriginalResponse = agentResponse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original non-structured response from the inner agent used by chat client to produce the structured output.
|
||||
/// </summary>
|
||||
public AgentResponse OriginalResponse { get; }
|
||||
}
|
||||
@@ -12,20 +12,23 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? th
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// Create the agent
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Start a new session for the agent conversation.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with a new session.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
|
||||
|
||||
// Serialize the session state to a JsonElement, so it can be stored for later use.
|
||||
JsonElement serializedSession = session.Serialize();
|
||||
JsonElement serializedSession = await agent.SerializeSessionAsync(session);
|
||||
|
||||
// Save the serialized session to a temporary file (for demonstration purposes).
|
||||
string tempFilePath = Path.GetTempFileName();
|
||||
|
||||
+74
-34
@@ -2,7 +2,9 @@
|
||||
|
||||
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with a conversation that can be persisted to disk.
|
||||
// This sample shows how to create and use a simple AI agent with custom ChatHistoryProvider that stores chat history in a custom storage location.
|
||||
// The state of the custom ChatHistoryProvider (SessionDbKey) is stored in the AgentSession's StateBag, so that when the session is resumed later,
|
||||
// the chat history can be retrieved from the custom storage location.
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
@@ -23,23 +25,23 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
VectorStore vectorStore = new InMemoryVectorStore();
|
||||
|
||||
// Create the agent
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(
|
||||
// Create a new ChatHistoryProvider for this agent that stores chat history in a vector store.
|
||||
// Each session must get its own copy of the VectorChatHistoryProvider, since the provider
|
||||
// also contains the id that the chat history is stored under.
|
||||
new VectorChatHistoryProvider(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions))
|
||||
// Create a new ChatHistoryProvider for this agent that stores chat history in a vector store.
|
||||
ChatHistoryProvider = new VectorChatHistoryProvider(vectorStore)
|
||||
});
|
||||
|
||||
// Start a new session for the agent conversation.
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the session that stores chat history in the vector store.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
|
||||
@@ -47,7 +49,7 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session
|
||||
// Serialize the session state, so it can be stored for later use.
|
||||
// Since the chat history is stored in the vector store, the serialized session
|
||||
// only contains the guid that the messages are stored under in the vector store.
|
||||
JsonElement serializedSession = session.Serialize();
|
||||
JsonElement serializedSession = await agent.SerializeSessionAsync(session);
|
||||
|
||||
Console.WriteLine("\n--- Serialized session ---\n");
|
||||
Console.WriteLine(JsonSerializer.Serialize(serializedSession, new JsonSerializerOptions { WriteIndented = true }));
|
||||
@@ -61,51 +63,78 @@ AgentSession resumedSession = await agent.DeserializeSessionAsync(serializedSess
|
||||
// Run the agent with the session that stores chat history in the vector store a second time.
|
||||
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedSession));
|
||||
|
||||
// We can access the VectorChatHistoryProvider via the session's GetService method if we need to read the key under which chat history is stored.
|
||||
var chatHistoryProvider = resumedSession.GetService<VectorChatHistoryProvider>()!;
|
||||
Console.WriteLine($"\nSession is stored in vector store under key: {chatHistoryProvider.SessionDbKey}");
|
||||
// We can access the VectorChatHistoryProvider via the agent's GetService method
|
||||
// if we need to read the key under which chat history is stored. The key is stored
|
||||
// in the session state, and therefore we need to provide the session when reading it.
|
||||
var chatHistoryProvider = agent.GetService<VectorChatHistoryProvider>()!;
|
||||
Console.WriteLine($"\nSession is stored in vector store under key: {chatHistoryProvider.GetSessionDbKey(resumedSession)}");
|
||||
|
||||
namespace SampleApp
|
||||
{
|
||||
/// <summary>
|
||||
/// A sample implementation of <see cref="ChatHistoryProvider"/> that stores chat history in a vector store.
|
||||
/// State (the session DB key) is stored in the <see cref="AgentSession.StateBag"/> so it roundtrips
|
||||
/// automatically with session serialization.
|
||||
/// </summary>
|
||||
internal sealed class VectorChatHistoryProvider : ChatHistoryProvider
|
||||
{
|
||||
private readonly VectorStore _vectorStore;
|
||||
private readonly Func<AgentSession?, State> _stateInitializer;
|
||||
private readonly string _stateKey;
|
||||
|
||||
public VectorChatHistoryProvider(VectorStore vectorStore, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
/// <inheritdoc />
|
||||
public override string StateKey => this._stateKey;
|
||||
|
||||
public VectorChatHistoryProvider(
|
||||
VectorStore vectorStore,
|
||||
Func<AgentSession?, State>? stateInitializer = null,
|
||||
string? stateKey = null)
|
||||
{
|
||||
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
|
||||
|
||||
if (serializedState.ValueKind is JsonValueKind.String)
|
||||
{
|
||||
// Here we can deserialize the session id so that we can access the same messages as before the suspension.
|
||||
this.SessionDbKey = serializedState.Deserialize<string>();
|
||||
}
|
||||
this._stateInitializer = stateInitializer ?? (_ => new State(Guid.NewGuid().ToString("N")));
|
||||
this._stateKey = stateKey ?? base.StateKey;
|
||||
}
|
||||
|
||||
public string? SessionDbKey { get; private set; }
|
||||
public string GetSessionDbKey(AgentSession session)
|
||||
=> this.GetOrInitializeState(session).SessionDbKey;
|
||||
|
||||
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
private State GetOrInitializeState(AgentSession? session)
|
||||
{
|
||||
if (session?.StateBag.TryGetValue<State>(this._stateKey, out var state) is true && state is not null)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
state = this._stateInitializer(session);
|
||||
if (session is not null)
|
||||
{
|
||||
session.StateBag.SetValue(this._stateKey, state);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
var records = await collection
|
||||
.GetAsync(
|
||||
x => x.SessionId == this.SessionDbKey, 10,
|
||||
x => x.SessionId == state.SessionDbKey, 10,
|
||||
new() { OrderBy = x => x.Descending(y => y.Timestamp) },
|
||||
cancellationToken)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var messages = records.ConvertAll(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!)
|
||||
;
|
||||
var messages = records.ConvertAll(x => JsonSerializer.Deserialize<ChatMessage>(x.SerializedMessage!)!);
|
||||
messages.Reverse();
|
||||
return messages;
|
||||
return messages
|
||||
.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!))
|
||||
.Concat(context.RequestMessages);
|
||||
}
|
||||
|
||||
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Don't store messages if the request failed.
|
||||
if (context.InvokeException is not null)
|
||||
@@ -113,28 +142,39 @@ namespace SampleApp
|
||||
return;
|
||||
}
|
||||
|
||||
this.SessionDbKey ??= Guid.NewGuid().ToString("N");
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
// Add both request and response messages to the store
|
||||
// Add both request and response messages to the store, excluding messages that came from chat history.
|
||||
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
|
||||
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
|
||||
var allNewMessages = context.RequestMessages
|
||||
.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
|
||||
.Concat(context.ResponseMessages ?? []);
|
||||
|
||||
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
|
||||
{
|
||||
Key = this.SessionDbKey + x.MessageId,
|
||||
Key = state.SessionDbKey + x.MessageId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
SessionId = this.SessionDbKey,
|
||||
SessionId = state.SessionDbKey,
|
||||
SerializedMessage = JsonSerializer.Serialize(x),
|
||||
MessageText = x.Text
|
||||
}), cancellationToken);
|
||||
}
|
||||
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
|
||||
// We have to serialize the session id, so that on deserialization we can retrieve the messages using the same session id.
|
||||
JsonSerializer.SerializeToElement(this.SessionDbKey);
|
||||
/// <summary>
|
||||
/// Represents the per-session state stored in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
public sealed class State
|
||||
{
|
||||
public State(string sessionDbKey)
|
||||
{
|
||||
this.SessionDbKey = sessionDbKey ?? throw new ArgumentNullException(nameof(sessionDbKey));
|
||||
}
|
||||
|
||||
public string SessionDbKey { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The data structure used to store chat history items in the vector store.
|
||||
@@ -27,7 +27,10 @@ if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
|
||||
using var tracerProvider = tracerProviderBuilder.Build();
|
||||
|
||||
// Create the agent, and enable OpenTelemetry instrumentation.
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
// 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 agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker")
|
||||
.AsBuilder()
|
||||
|
||||
@@ -21,9 +21,12 @@ HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddSingleton(new ChatClientAgentOptions() { Name = "Joker", ChatOptions = new() { Instructions = "You are good at telling jokes." } });
|
||||
|
||||
// Add a chat client to the service collection.
|
||||
// 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.
|
||||
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient());
|
||||
|
||||
@@ -49,7 +52,7 @@ internal sealed class SampleService(AIAgent agent, IHostApplicationLifetime appL
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Create a session that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
|
||||
this._session = await agent.GetNewSessionAsync(cancellationToken);
|
||||
this._session = await agent.CreateSessionAsync(cancellationToken);
|
||||
_ = this.RunAsync(appLifetime.ApplicationStopping);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ using ModelContextProtocol.Server;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
// 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.
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new DefaultAzureCredential());
|
||||
|
||||
// Create a server side persistent agent
|
||||
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
|
||||
|
||||
@@ -11,7 +11,10 @@ using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||
|
||||
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
// 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.
|
||||
var agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
name: "VisionAgent",
|
||||
@@ -22,7 +25,7 @@ ChatMessage message = new(ChatRole.User, [
|
||||
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
|
||||
]);
|
||||
|
||||
var session = await agent.GetNewSessionAsync();
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
await foreach (var update in agent.RunStreamingAsync(message, session))
|
||||
{
|
||||
|
||||
@@ -17,9 +17,12 @@ static string GetWeather([Description("The location to get the weather for.")] s
|
||||
=> $"The weather in {location} is cloudy with a high of 15°C.";
|
||||
|
||||
// Create the chat client and agent, and provide the function tool to the agent.
|
||||
// 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 weatherAgent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
instructions: "You answer questions about the weather.",
|
||||
@@ -30,7 +33,7 @@ AIAgent weatherAgent = new AzureOpenAIClient(
|
||||
// Create the main agent, and provide the weather agent as a function tool.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are a helpful assistant who responds in French.", tools: [weatherAgent.AsAIFunction()]);
|
||||
|
||||
|
||||
+8
-5
@@ -19,9 +19,12 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
|
||||
var stateStore = new Dictionary<string, JsonElement?>();
|
||||
|
||||
// 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 agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
name: "SpaceNovelWriter",
|
||||
@@ -32,7 +35,7 @@ AIAgent agent = new AzureOpenAIClient(
|
||||
// Enable background responses (only supported by {Azure}OpenAI Responses at this time).
|
||||
AgentRunOptions options = new() { AllowBackgroundResponses = true };
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Start the initial run.
|
||||
AgentResponse response = await agent.RunAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", session, options);
|
||||
@@ -40,7 +43,7 @@ AgentResponse response = await agent.RunAsync("Write a very long novel about a t
|
||||
// Poll for background responses until complete.
|
||||
while (response.ContinuationToken is not null)
|
||||
{
|
||||
PersistAgentState(session, response.ContinuationToken);
|
||||
await PersistAgentState(agent, session, response.ContinuationToken);
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
|
||||
@@ -52,9 +55,9 @@ while (response.ContinuationToken is not null)
|
||||
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
void PersistAgentState(AgentSession? session, ResponseContinuationToken? continuationToken)
|
||||
async Task PersistAgentState(AIAgent agent, AgentSession? session, ResponseContinuationToken? continuationToken)
|
||||
{
|
||||
stateStore["session"] = session!.Serialize();
|
||||
stateStore["session"] = await agent.SerializeSessionAsync(session!);
|
||||
stateStore["continuationToken"] = JsonSerializer.SerializeToElement(continuationToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? th
|
||||
var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||
|
||||
// Get a client to create/retrieve server side agents with
|
||||
var azureOpenAIClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
// 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.
|
||||
var azureOpenAIClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
[Description("Get the weather for a given location.")]
|
||||
@@ -45,7 +48,7 @@ var middlewareEnabledAgent = originalAgent
|
||||
.Use(GuardrailMiddleware, null)
|
||||
.Build();
|
||||
|
||||
var session = await middlewareEnabledAgent.GetNewSessionAsync();
|
||||
var session = await middlewareEnabledAgent.CreateSessionAsync();
|
||||
|
||||
Console.WriteLine("\n\n=== Example 1: Wording Guardrail ===");
|
||||
var guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful.");
|
||||
@@ -210,28 +213,25 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
|
||||
// This middleware handles Human in the loop console interaction for any user approval required during function calling.
|
||||
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
|
||||
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
|
||||
|
||||
var userInputRequests = response.UserInputRequests.ToList();
|
||||
// For simplicity, we are assuming here that only function approvals are pending.
|
||||
List<FunctionApprovalRequestContent> approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
|
||||
|
||||
while (userInputRequests.Count > 0)
|
||||
while (approvalRequests.Count > 0)
|
||||
{
|
||||
// Ask the user to approve each function call request.
|
||||
// For simplicity, we are assuming here that only function approval requests are being made.
|
||||
|
||||
// Pass the user input responses back to the agent for further processing.
|
||||
response.Messages = userInputRequests
|
||||
.OfType<FunctionApprovalRequestContent>()
|
||||
.Select(functionApprovalRequest =>
|
||||
response.Messages = approvalRequests
|
||||
.ConvertAll(functionApprovalRequest =>
|
||||
{
|
||||
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {functionApprovalRequest.FunctionCall.Name}");
|
||||
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
|
||||
})
|
||||
.ToList();
|
||||
});
|
||||
|
||||
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
|
||||
|
||||
userInputRequests = response.UserInputRequests.ToList();
|
||||
approvalRequests = response.Messages.SelectMany(m => m.Contents).OfType<FunctionApprovalRequestContent>().ToList();
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user