mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f958bf06ba | ||
|
|
443ed50f58 | ||
|
|
8b4f7d5e29 | ||
|
|
4b8a545589 | ||
|
|
5ab47596ff | ||
|
|
a32702cf38 | ||
|
|
8b743af217 | ||
|
|
0e152a0e33 | ||
|
|
3b77192ad0 | ||
|
|
defe0f1a89 | ||
|
|
85d70f01f6 | ||
|
|
6930c0f0b6 | ||
|
|
d83cf93f07 | ||
|
|
8783ac58f1 | ||
|
|
e15eab7da6 | ||
|
|
19a9e13788 | ||
|
|
b0a7a1fcb8 | ||
|
|
a841bdd1cc | ||
|
|
d46adffe6c | ||
|
|
b0b5777363 | ||
|
|
37b4cfd024 | ||
|
|
ff9343d7cc | ||
|
|
8ff34f9a43 | ||
|
|
e3f8bfc645 | ||
|
|
b4f2709b6d | ||
|
|
e5c11d38d6 | ||
|
|
a71f768331 | ||
|
|
0298e0a401 | ||
|
|
ca1532cf22 | ||
|
|
360839782c | ||
|
|
ee53fe4666 | ||
|
|
3cd805f0bf | ||
|
|
c7ddb8aa14 | ||
|
|
d5527982b6 | ||
|
|
ec1c5e9c11 | ||
|
|
06cdcb93f0 | ||
|
|
6adcac2e97 | ||
|
|
8fca71e5ad | ||
|
|
2bde58f915 | ||
|
|
03a403d2fa | ||
|
|
e319707058 | ||
|
|
54f482df73 | ||
|
|
754dfb2c9d | ||
|
|
b15466f058 | ||
|
|
3a7047f6e4 | ||
|
|
2f06fe557a | ||
|
|
1dbf3fd5cf | ||
|
|
0132cf65e4 | ||
|
|
a53a3c7af8 | ||
|
|
3c322c91e7 | ||
|
|
958a488f96 | ||
|
|
11d6dcfe80 | ||
|
|
3139347526 | ||
|
|
3c379718e9 | ||
|
|
a7298757f5 | ||
|
|
0dcebc6eae | ||
|
|
e0ff153ee9 | ||
|
|
e008144187 | ||
|
|
0fc7933a92 | ||
|
|
d7434d59ce | ||
|
|
eb1117fff4 | ||
|
|
16230d3b20 | ||
|
|
8d53b20026 | ||
|
|
c376868ec9 | ||
|
|
8bb9927f3c | ||
|
|
194486c4cc | ||
|
|
0413f4220a | ||
|
|
67e83042cf | ||
|
|
5da1c2fd4c | ||
|
|
989b6ebe71 | ||
|
|
3481914981 | ||
|
|
4c6a5d4aa1 | ||
|
|
191779ce80 | ||
|
|
88b98aacd1 | ||
|
|
291547ad02 | ||
|
|
9498c8425e | ||
|
|
93825265cf | ||
|
|
9d86adfcb2 | ||
|
|
570bed9ff6 | ||
|
|
eff5aee5aa |
@@ -28,6 +28,18 @@ runs:
|
||||
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
|
||||
echo "Azurite (Azure Storage emulator) is ready"
|
||||
- name: Start Redis
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=redis)" ]; then
|
||||
echo "Stopping and removing existing Redis"
|
||||
docker rm -f redis
|
||||
fi
|
||||
echo "Starting Redis"
|
||||
docker run -d --name redis -p 6379:6379 redis:latest
|
||||
echo "Waiting for Redis to be ready"
|
||||
timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
|
||||
echo "Redis is ready"
|
||||
- name: Install Azure Functions Core Tools
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -839,7 +839,7 @@ var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 8000,
|
||||
// Breaking glass to access provider-specific options
|
||||
RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions()
|
||||
RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
|
||||
{
|
||||
ReasoningOptions = new()
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -35,19 +35,25 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
|
||||
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
dotnet:
|
||||
- 'dotnet/**'
|
||||
cosmosdb:
|
||||
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
|
||||
# run only if 'dotnet' files were changed
|
||||
- name: dotnet tests
|
||||
if: steps.filter.outputs.dotnet == 'true'
|
||||
run: echo "Dotnet file"
|
||||
- name: dotnet CosmosDB tests
|
||||
if: steps.filter.outputs.cosmosdb == 'true'
|
||||
run: echo "Dotnet CosmosDB changes"
|
||||
# run only if not 'dotnet' files were changed
|
||||
- name: not dotnet tests
|
||||
if: steps.filter.outputs.dotnet != 'true'
|
||||
@@ -68,7 +74,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -77,6 +83,16 @@ jobs:
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
|
||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.0.1
|
||||
@@ -123,17 +139,7 @@ jobs:
|
||||
popd
|
||||
popd
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# Start Cosmos DB Emulator for Cosmos-based unit tests (only on Windows)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
|
||||
- name: Run Unit Tests
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -225,7 +231,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage report artifact
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
|
||||
path: ./TestResults/Reports # Directory containing files to upload
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
# check out the latest version of the code
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
outputs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
outputs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe azure-ai-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
working-directory: ./python
|
||||
- name: Test Azure AI samples
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download coverage report
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: "3.10"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
# Save the PR number to a file since the workflow_run event
|
||||
# in the coverage report workflow does not have access to it
|
||||
- name: Save PR number
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Run all tests with coverage report
|
||||
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: |
|
||||
python/python-coverage.xml
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Declarative Agents
|
||||
|
||||
This folder contains sample agent definitions than be ran 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/getting_started/declarative/).
|
||||
|
||||
@@ -10,19 +10,19 @@ model:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
kind: key
|
||||
apiKey: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# Time-To-Live (TTL) for durable agent sessions
|
||||
|
||||
## Overview
|
||||
|
||||
The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity.
|
||||
|
||||
## What is TTL?
|
||||
|
||||
Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime.
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions
|
||||
- **Cost optimization**: Reduces storage costs by automatically removing unused session state
|
||||
- **Resource management**: Prevents unbounded growth of agent session state in storage
|
||||
- **Configurable**: Set TTL globally or per-agent type to match your application's needs
|
||||
|
||||
## Configuration
|
||||
|
||||
TTL can be configured at two levels:
|
||||
|
||||
1. **Global default TTL**: Applies to all agent sessions unless overridden
|
||||
2. **Per-agent type TTL**: Overrides the global default for specific agent types
|
||||
|
||||
Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes.
|
||||
|
||||
> [!NOTE]
|
||||
> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution.
|
||||
|
||||
### Default values
|
||||
|
||||
- **Default TTL**: 14 days
|
||||
- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases)
|
||||
|
||||
### Configuration examples
|
||||
|
||||
#### .NET
|
||||
|
||||
```csharp
|
||||
// Configure global default TTL and minimum signal delay
|
||||
services.ConfigureDurableAgents(
|
||||
options =>
|
||||
{
|
||||
// Set global default TTL to 7 days
|
||||
options.DefaultTimeToLive = TimeSpan.FromDays(7);
|
||||
|
||||
// Add agents (will use global default TTL)
|
||||
options.AddAIAgent(myAgent);
|
||||
});
|
||||
|
||||
// Configure per-agent TTL
|
||||
services.ConfigureDurableAgents(
|
||||
options =>
|
||||
{
|
||||
options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default
|
||||
|
||||
// Agent with custom TTL of 1 day
|
||||
options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));
|
||||
|
||||
// Agent with custom TTL of 90 days
|
||||
options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));
|
||||
|
||||
// Agent using global default (14 days)
|
||||
options.AddAIAgent(defaultAgent);
|
||||
});
|
||||
|
||||
// Disable TTL for specific agents by setting TTL to null
|
||||
services.ConfigureDurableAgents(
|
||||
options =>
|
||||
{
|
||||
options.DefaultTimeToLive = TimeSpan.FromDays(14);
|
||||
|
||||
// Agent with no TTL (never expires)
|
||||
options.AddAIAgent(permanentAgent, timeToLive: null);
|
||||
});
|
||||
```
|
||||
|
||||
## How TTL works
|
||||
|
||||
The following sections describe how TTL works in detail.
|
||||
|
||||
### Expiration tracking
|
||||
|
||||
Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message:
|
||||
|
||||
1. When a message is sent to an agent session, the expiration time is set to `current time + TTL`
|
||||
2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints)
|
||||
3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time.
|
||||
|
||||
### State deletion
|
||||
|
||||
When an agent session expires, its entire state is deleted, including:
|
||||
|
||||
- Conversation history
|
||||
- Any custom state data
|
||||
- Expiration timestamps
|
||||
|
||||
After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history.
|
||||
|
||||
## Behavior examples
|
||||
|
||||
The following examples illustrate how TTL works in different scenarios.
|
||||
|
||||
### Example 1: Agent session expires after TTL
|
||||
|
||||
1. Agent configured with 30-day TTL
|
||||
2. User sends message at Day 0 → agent session created, expiration set to Day 30
|
||||
3. No further messages sent
|
||||
4. At Day 30 → Agent session is deleted
|
||||
5. User sends message at Day 31 → New agent session created with fresh conversation history
|
||||
|
||||
### Example 2: TTL reset on interaction
|
||||
|
||||
1. Agent configured with 30-day TTL
|
||||
2. User sends message at Day 0 → agent session created, expiration set to Day 30
|
||||
3. User sends message at Day 15 → Expiration reset to Day 45
|
||||
4. User sends message at Day 40 → Expiration reset to Day 70
|
||||
5. Agent session remains active as long as there are regular interactions
|
||||
|
||||
## Logging
|
||||
|
||||
The TTL feature includes comprehensive logging to track state changes:
|
||||
|
||||
- **Expiration time updated**: Logged when TTL expiration time is set or updated
|
||||
- **Deletion scheduled**: Logged when a deletion check signal is scheduled
|
||||
- **Deletion check**: Logged when a deletion check operation runs
|
||||
- **Session expired**: Logged when an agent session is deleted due to expiration
|
||||
- **TTL rescheduled**: Logged when a deletion signal is rescheduled
|
||||
|
||||
These logs help monitor TTL behavior and troubleshoot any issues.
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state.
|
||||
|
||||
2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes.
|
||||
|
||||
3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed.
|
||||
|
||||
4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods.
|
||||
|
||||
## Limitations
|
||||
|
||||
- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp.
|
||||
- Deletion checks are durably scheduled operations and may have slight delays depending on system load.
|
||||
- Once an agent session is deleted, its conversation history cannot be recovered.
|
||||
- TTL deletion requires at least one worker to be available to process the deletion operation message.
|
||||
@@ -209,6 +209,7 @@ dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on ob
|
||||
dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates
|
||||
dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter
|
||||
dotnet_diagnostic.CA2249.severity = suggestion # Consider using 'Contains' method instead of 'IndexOf' method
|
||||
dotnet_diagnostic.CA2252.severity = none # Requires preview
|
||||
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
|
||||
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
|
||||
dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="11.0.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.0.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.3" />
|
||||
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<!-- Google Gemini -->
|
||||
<PackageVersion Include="Google.GenAI" Version="0.6.0" />
|
||||
@@ -61,10 +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.1.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
|
||||
<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" />
|
||||
@@ -101,11 +100,10 @@
|
||||
<!-- MCP -->
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.7" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.7.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.8.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
|
||||
<!-- Workflows -->
|
||||
@@ -114,19 +112,21 @@
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.16.2" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.16.2-preview.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.16.2" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.16.2-preview.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
|
||||
<!-- Azure Functions -->
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.9.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Mcp" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
|
||||
<!-- Redis -->
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.22" />
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
@@ -129,6 +130,7 @@
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/Purview/" />
|
||||
<Folder Name="/Samples/Purview/AgentWithPurview/">
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251204.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251204.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251204.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251219.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251219.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251219.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -19,12 +19,12 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -20,12 +20,12 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -19,12 +19,12 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenAPI" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AgentWebChat.AgentHost.Utilities;
|
||||
using Azure;
|
||||
using Azure.AI.Inference;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OllamaSharp;
|
||||
|
||||
@@ -24,7 +22,6 @@ public static class ChatClientExtensions
|
||||
ClientChatProvider.Ollama => builder.AddOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddAzureOpenAIClient(connectionName).AddChatClient(connectionInfo.SelectedModel),
|
||||
ClientChatProvider.AzureAIInference => builder.AddAzureInferenceClient(connectionName, connectionInfo),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
@@ -44,16 +41,6 @@ public static class ChatClientExtensions
|
||||
})
|
||||
.AddChatClient(connectionInfo.SelectedModel);
|
||||
|
||||
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
|
||||
builder.Services.AddChatClient(sp =>
|
||||
{
|
||||
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
|
||||
|
||||
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
|
||||
|
||||
return client.AsIChatClient(connectionInfo.SelectedModel);
|
||||
});
|
||||
|
||||
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
@@ -83,7 +70,6 @@ public static class ChatClientExtensions
|
||||
ClientChatProvider.Ollama => builder.AddKeyedOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddKeyedOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddKeyedAzureOpenAIClient(connectionName).AddKeyedChatClient(connectionName, connectionInfo.SelectedModel),
|
||||
ClientChatProvider.AzureAIInference => builder.AddKeyedAzureInferenceClient(connectionName, connectionInfo),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
@@ -103,16 +89,6 @@ public static class ChatClientExtensions
|
||||
})
|
||||
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
|
||||
|
||||
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
|
||||
builder.Services.AddKeyedChatClient(connectionName, sp =>
|
||||
{
|
||||
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
|
||||
|
||||
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
|
||||
|
||||
return client.AsIChatClient(connectionInfo.SelectedModel);
|
||||
});
|
||||
|
||||
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
|
||||
@@ -27,7 +27,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
|
||||
Transport = new HttpClientPipelineTransport(httpClient)
|
||||
};
|
||||
|
||||
var openAiClient = new OpenAIResponseClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var chatOptions = new ChatOptions()
|
||||
{
|
||||
ConversationId = threadId
|
||||
|
||||
@@ -32,6 +32,6 @@ AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstruct
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.Build();
|
||||
app.Run();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>ReliableStreaming</AssemblyName>
|
||||
<RootNamespace>ReliableStreaming</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Redis for reliable streaming -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,320 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ReliableStreaming;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP trigger functions for reliable streaming of durable agent responses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class exposes two endpoints:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Create</term>
|
||||
/// <description>Starts an agent run and streams responses. The response format depends on the
|
||||
/// <c>Accept</c> header: <c>text/plain</c> returns raw text (ideal for terminals), while
|
||||
/// <c>text/event-stream</c> or any other value returns Server-Sent Events (SSE).</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Stream</term>
|
||||
/// <description>Resumes a stream from a cursor position, enabling reliable message delivery</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class FunctionTriggers
|
||||
{
|
||||
private readonly RedisStreamResponseHandler _streamHandler;
|
||||
private readonly ILogger<FunctionTriggers> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FunctionTriggers"/> class.
|
||||
/// </summary>
|
||||
/// <param name="streamHandler">The Redis stream handler for reading/writing agent responses.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public FunctionTriggers(RedisStreamResponseHandler streamHandler, ILogger<FunctionTriggers> logger)
|
||||
{
|
||||
this._streamHandler = streamHandler;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new agent session, starts an agent run with the provided prompt,
|
||||
/// and streams the response back to the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The response format depends on the <c>Accept</c> header:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>text/plain</c>: Returns raw text output, ideal for terminal display with curl</item>
|
||||
/// <item><c>text/event-stream</c> or other: Returns Server-Sent Events (SSE) with cursor support</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The response includes an <c>x-conversation-id</c> header containing the conversation ID.
|
||||
/// For SSE responses, clients can use this conversation ID to resume the stream if disconnected
|
||||
/// by calling the <see cref="StreamAsync"/> endpoint with the conversation ID and the last received cursor.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each SSE event contains the following fields:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>id</c>: The Redis stream entry ID (use as cursor for resumption)</item>
|
||||
/// <item><c>event</c>: Either "message" for content or "done" for stream completion</item>
|
||||
/// <item><c>data</c>: The text content of the response chunk</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="request">The HTTP request containing the prompt in the body.</param>
|
||||
/// <param name="durableClient">The Durable Task client for signaling agents.</param>
|
||||
/// <param name="context">The function invocation context.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A streaming response in the format specified by the Accept header.</returns>
|
||||
[Function(nameof(CreateAsync))]
|
||||
public async Task<IActionResult> CreateAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/create")] HttpRequest request,
|
||||
[DurableClient] DurableTaskClient durableClient,
|
||||
FunctionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Read the prompt from the request body
|
||||
string prompt = await new StreamReader(request.Body).ReadToEndAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
return new BadRequestObjectResult("Request body must contain a prompt.");
|
||||
}
|
||||
|
||||
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
|
||||
|
||||
// Create a new agent thread
|
||||
AgentThread thread = agentProxy.GetNewThread();
|
||||
AgentThreadMetadata metadata = thread.GetService<AgentThreadMetadata>()
|
||||
?? throw new InvalidOperationException("Failed to get AgentThreadMetadata from new thread.");
|
||||
|
||||
this._logger.LogInformation("Creating new agent session: {ConversationId}", metadata.ConversationId);
|
||||
|
||||
// Run the agent in the background (fire-and-forget)
|
||||
DurableAgentRunOptions options = new() { IsFireAndForget = true };
|
||||
await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
|
||||
|
||||
this._logger.LogInformation("Agent run started for session: {ConversationId}", metadata.ConversationId);
|
||||
|
||||
// Check Accept header to determine response format
|
||||
// text/plain = raw text output (ideal for terminals)
|
||||
// text/event-stream or other = SSE format (supports resumption)
|
||||
string? acceptHeader = request.Headers.Accept.FirstOrDefault();
|
||||
bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
|
||||
|
||||
return await this.StreamToClientAsync(
|
||||
conversationId: metadata.ConversationId!, cursor: null, useSseFormat, request.HttpContext, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes streaming from a specific cursor position for an existing session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Use this endpoint to resume a stream after disconnection. Pass the conversation ID
|
||||
/// (from the <c>x-conversation-id</c> response header) and the last received cursor
|
||||
/// (Redis stream entry ID) to continue from where you left off.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If no cursor is provided, streaming starts from the beginning of the stream.
|
||||
/// This allows clients to replay the entire response if needed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The response format depends on the <c>Accept</c> header:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>text/plain</c>: Returns raw text output, ideal for terminal display with curl</item>
|
||||
/// <item><c>text/event-stream</c> or other: Returns Server-Sent Events (SSE) with cursor support</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="request">The HTTP request. Use the <c>cursor</c> query parameter to specify the cursor position.</param>
|
||||
/// <param name="conversationId">The conversation ID to stream from.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A streaming response in the format specified by the Accept header.</returns>
|
||||
[Function(nameof(StreamAsync))]
|
||||
public async Task<IActionResult> StreamAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "agent/stream/{conversationId}")] HttpRequest request,
|
||||
string conversationId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conversationId))
|
||||
{
|
||||
return new BadRequestObjectResult("Conversation ID is required.");
|
||||
}
|
||||
|
||||
// Get the cursor from query string (optional)
|
||||
string? cursor = request.Query["cursor"].FirstOrDefault();
|
||||
|
||||
this._logger.LogInformation(
|
||||
"Resuming stream for conversation {ConversationId} from cursor: {Cursor}",
|
||||
conversationId,
|
||||
cursor ?? "(beginning)");
|
||||
|
||||
// Check Accept header to determine response format
|
||||
// text/plain = raw text output (ideal for terminals)
|
||||
// text/event-stream or other = SSE format (supports cursor-based resumption)
|
||||
string? acceptHeader = request.Headers.Accept.FirstOrDefault();
|
||||
bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
|
||||
|
||||
return await this.StreamToClientAsync(conversationId, cursor, useSseFormat, request.HttpContext, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams chunks from the Redis stream to the HTTP response.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The conversation ID to stream from.</param>
|
||||
/// <param name="cursor">Optional cursor to resume from. If null, streams from the beginning.</param>
|
||||
/// <param name="useSseFormat">True to use SSE format, false for plain text.</param>
|
||||
/// <param name="httpContext">The HTTP context for writing the response.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An empty result after streaming completes.</returns>
|
||||
private async Task<IActionResult> StreamToClientAsync(
|
||||
string conversationId,
|
||||
string? cursor,
|
||||
bool useSseFormat,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Set response headers based on format
|
||||
httpContext.Response.Headers.ContentType = useSseFormat
|
||||
? "text/event-stream"
|
||||
: "text/plain; charset=utf-8";
|
||||
httpContext.Response.Headers.CacheControl = "no-cache";
|
||||
httpContext.Response.Headers.Connection = "keep-alive";
|
||||
httpContext.Response.Headers["x-conversation-id"] = conversationId;
|
||||
|
||||
// Disable response buffering if supported
|
||||
httpContext.Features.Get<IHttpResponseBodyFeature>()?.DisableBuffering();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (StreamChunk chunk in this._streamHandler.ReadStreamAsync(
|
||||
conversationId,
|
||||
cursor,
|
||||
cancellationToken))
|
||||
{
|
||||
if (chunk.Error != null)
|
||||
{
|
||||
this._logger.LogWarning("Stream error for conversation {ConversationId}: {Error}", conversationId, chunk.Error);
|
||||
await WriteErrorAsync(httpContext.Response, chunk.Error, useSseFormat, cancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
if (chunk.IsDone)
|
||||
{
|
||||
await WriteEndOfStreamAsync(httpContext.Response, chunk.EntryId, useSseFormat, cancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
if (chunk.Text != null)
|
||||
{
|
||||
await WriteChunkAsync(httpContext.Response, chunk, useSseFormat, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
this._logger.LogInformation("Client disconnected from stream {ConversationId}", conversationId);
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a text chunk to the response.
|
||||
/// </summary>
|
||||
private static async Task WriteChunkAsync(
|
||||
HttpResponse response,
|
||||
StreamChunk chunk,
|
||||
bool useSseFormat,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (useSseFormat)
|
||||
{
|
||||
await WriteSSEEventAsync(response, "message", chunk.Text!, chunk.EntryId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await response.WriteAsync(chunk.Text!, cancellationToken);
|
||||
}
|
||||
|
||||
await response.Body.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an end-of-stream marker to the response.
|
||||
/// </summary>
|
||||
private static async Task WriteEndOfStreamAsync(
|
||||
HttpResponse response,
|
||||
string entryId,
|
||||
bool useSseFormat,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (useSseFormat)
|
||||
{
|
||||
await WriteSSEEventAsync(response, "done", "[DONE]", entryId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await response.WriteAsync("\n", cancellationToken);
|
||||
}
|
||||
|
||||
await response.Body.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error message to the response.
|
||||
/// </summary>
|
||||
private static async Task WriteErrorAsync(
|
||||
HttpResponse response,
|
||||
string error,
|
||||
bool useSseFormat,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (useSseFormat)
|
||||
{
|
||||
await WriteSSEEventAsync(response, "error", error, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
await response.WriteAsync($"\n[Error: {error}]\n", cancellationToken);
|
||||
}
|
||||
|
||||
await response.Body.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Server-Sent Event to the response stream.
|
||||
/// </summary>
|
||||
private static async Task WriteSSEEventAsync(
|
||||
HttpResponse response,
|
||||
string eventType,
|
||||
string data,
|
||||
string? id)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
|
||||
// Include the ID if provided (used as cursor for resumption)
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
sb.AppendLine($"id: {id}");
|
||||
}
|
||||
|
||||
sb.AppendLine($"event: {eventType}");
|
||||
sb.AppendLine($"data: {data}");
|
||||
sb.AppendLine(); // Empty line marks end of event
|
||||
|
||||
await response.WriteAsync(sb.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams.
|
||||
// It exposes two HTTP endpoints:
|
||||
// 1. Create - Starts an agent run and streams responses back via Server-Sent Events (SSE)
|
||||
// 2. Stream - Resumes a stream from a specific cursor position, enabling reliable message delivery
|
||||
//
|
||||
// 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.
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
using ReliableStreaming;
|
||||
using StackExchange.Redis;
|
||||
|
||||
// Get the Azure OpenAI endpoint and deployment name from environment variables.
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
|
||||
|
||||
// Get Redis connection string from environment variable.
|
||||
string redisConnectionString = Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING")
|
||||
?? "localhost:6379";
|
||||
|
||||
// Get the Redis stream TTL from environment variable (default: 10 minutes).
|
||||
int redisStreamTtlMinutes = int.TryParse(
|
||||
Environment.GetEnvironmentVariable("REDIS_STREAM_TTL_MINUTES"),
|
||||
out int ttlMinutes) ? ttlMinutes : 10;
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
|
||||
// Travel Planner agent instructions - designed to produce longer responses for demonstrating streaming.
|
||||
const string TravelPlannerName = "TravelPlanner";
|
||||
const string TravelPlannerInstructions =
|
||||
"""
|
||||
You are an expert travel planner who creates detailed, personalized travel itineraries.
|
||||
When asked to plan a trip, you should:
|
||||
1. Create a comprehensive day-by-day itinerary
|
||||
2. Include specific recommendations for activities, restaurants, and attractions
|
||||
3. Provide practical tips for each destination
|
||||
4. Consider weather and local events when making recommendations
|
||||
5. Include estimated times and logistics between activities
|
||||
|
||||
Always use the available tools to get current weather forecasts and local events
|
||||
for the destination to make your recommendations more relevant and timely.
|
||||
|
||||
Format your response with clear headings for each day and include emoji icons
|
||||
to make the itinerary easy to scan and visually appealing.
|
||||
""";
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
FunctionsApplicationBuilder builder = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options =>
|
||||
{
|
||||
// Define the Travel Planner agent with tools for weather and events
|
||||
options.AddAIAgentFactory(TravelPlannerName, sp =>
|
||||
{
|
||||
return client.GetChatClient(deploymentName).CreateAIAgent(
|
||||
instructions: TravelPlannerInstructions,
|
||||
name: TravelPlannerName,
|
||||
services: sp,
|
||||
tools: [
|
||||
AIFunctionFactory.Create(TravelTools.GetWeatherForecast),
|
||||
AIFunctionFactory.Create(TravelTools.GetLocalEvents),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// Register Redis connection as a singleton
|
||||
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||
ConnectionMultiplexer.Connect(redisConnectionString));
|
||||
|
||||
// Register the Redis stream response handler - this captures agent responses
|
||||
// and publishes them to Redis Streams for reliable delivery.
|
||||
// Registered as both the concrete type (for FunctionTriggers) and the interface (for the agent framework).
|
||||
builder.Services.AddSingleton(sp =>
|
||||
new RedisStreamResponseHandler(
|
||||
sp.GetRequiredService<IConnectionMultiplexer>(),
|
||||
TimeSpan.FromMinutes(redisStreamTtlMinutes)));
|
||||
builder.Services.AddSingleton<IAgentResponseHandler>(sp =>
|
||||
sp.GetRequiredService<RedisStreamResponseHandler>());
|
||||
|
||||
using IHost app = builder.Build();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,264 @@
|
||||
# Reliable Streaming with Redis
|
||||
|
||||
This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams as a message broker. It enables clients to disconnect and reconnect to ongoing agent responses without losing messages, inspired by [OpenAI's background mode](https://platform.openai.com/docs/guides/background) for the Responses API.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- **Reliable message delivery**: Agent responses are persisted to Redis Streams, allowing clients to resume from any point
|
||||
- **Content negotiation**: Use `Accept: text/plain` for raw terminal output, or `Accept: text/event-stream` for SSE format
|
||||
- **Server-Sent Events (SSE)**: Standard streaming format that works with `curl`, browsers, and most HTTP clients
|
||||
- **Cursor-based resumption**: Each SSE event includes an `id` field that can be used to resume the stream
|
||||
- **Fire-and-forget agent invocation**: The agent runs in the background while the client streams from Redis via an HTTP trigger function
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
### Additional Requirements: Redis
|
||||
|
||||
This sample requires a Redis instance. Start a local Redis instance using Docker:
|
||||
|
||||
```bash
|
||||
docker run -d --name redis -p 6379:6379 redis:latest
|
||||
```
|
||||
|
||||
To verify Redis is running:
|
||||
|
||||
```bash
|
||||
docker ps | grep redis
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
Start the Azure Functions host:
|
||||
|
||||
```bash
|
||||
func start
|
||||
```
|
||||
|
||||
### 1. Test Streaming with curl
|
||||
|
||||
Open a new terminal and start a travel planning request. Use the `-i` flag to see response headers (including the conversation ID) and `Accept: text/plain` for raw text output:
|
||||
|
||||
**Bash (Linux/macOS/WSL):**
|
||||
|
||||
```bash
|
||||
curl -i -N -X POST http://localhost:7071/api/agent/create \
|
||||
-H "Content-Type: text/plain" \
|
||||
-H "Accept: text/plain" \
|
||||
-d "Plan a 7-day trip to Tokyo, Japan for next month. Include daily activities, restaurant recommendations, and tips for getting around."
|
||||
```
|
||||
|
||||
**PowerShell:**
|
||||
|
||||
```powershell
|
||||
curl -i -N -X POST http://localhost:7071/api/agent/create `
|
||||
-H "Content-Type: text/plain" `
|
||||
-H "Accept: text/plain" `
|
||||
-d "Plan a 7-day trip to Tokyo, Japan for next month. Include daily activities, restaurant recommendations, and tips for getting around."
|
||||
```
|
||||
|
||||
You'll first see the response headers, including:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
x-conversation-id: @dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890
|
||||
...
|
||||
```
|
||||
|
||||
Then the agent's response will stream to your terminal in chunks, similar to a ChatGPT-style experience (though not character-by-character).
|
||||
|
||||
> **Note:** The `-N` flag in curl disables output buffering, which is essential for seeing the stream in real-time. The `-i` flag includes the HTTP headers in the output.
|
||||
|
||||
### 2. Demonstrate Stream Interruption and Resumption
|
||||
|
||||
This is the key feature of reliable streaming! Follow these steps to see it in action:
|
||||
|
||||
#### Step 1: Start a stream and note the conversation ID
|
||||
|
||||
Run the curl command from step 1. Watch for the `x-conversation-id` header in the response - **copy this value**, you'll need it to resume.
|
||||
|
||||
```text
|
||||
x-conversation-id: @dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890
|
||||
```
|
||||
|
||||
#### Step 2: Interrupt the stream
|
||||
|
||||
While the agent is still generating text, press **`Ctrl+C`** to interrupt the stream. The agent continues running in the background - your messages are being saved to Redis!
|
||||
|
||||
#### Step 3: Resume the stream
|
||||
|
||||
Use the conversation ID you copied to resume streaming from where you left off. Include the `Accept: text/plain` header to get raw text output:
|
||||
|
||||
**Bash (Linux/macOS/WSL):**
|
||||
|
||||
```bash
|
||||
# Replace with your actual conversation ID from the x-conversation-id header
|
||||
CONVERSATION_ID="@dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890"
|
||||
|
||||
curl -N -H "Accept: text/plain" "http://localhost:7071/api/agent/stream/${CONVERSATION_ID}"
|
||||
```
|
||||
|
||||
**PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Replace with your actual conversation ID from the x-conversation-id header
|
||||
$conversationId = "@dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890"
|
||||
|
||||
curl -N -H "Accept: text/plain" "http://localhost:7071/api/agent/stream/$conversationId"
|
||||
```
|
||||
|
||||
You'll see the **entire response replayed from the beginning**, including the parts you already received before interrupting.
|
||||
|
||||
#### Step 4 (Advanced): Resume from a specific cursor
|
||||
|
||||
If you're using SSE format, each event includes an `id` field that you can use as a cursor to resume from a specific point:
|
||||
|
||||
```bash
|
||||
# Resume from a specific cursor position
|
||||
curl -N "http://localhost:7071/api/agent/stream/${CONVERSATION_ID}?cursor=1734567890123-0"
|
||||
```
|
||||
|
||||
### 3. Alternative: SSE Format for Programmatic Clients
|
||||
|
||||
If you need the full Server-Sent Events format with cursors for resumable streaming, use `Accept: text/event-stream` (or omit the Accept header):
|
||||
|
||||
```bash
|
||||
curl -i -N -X POST http://localhost:7071/api/agent/create \
|
||||
-H "Content-Type: text/plain" \
|
||||
-H "Accept: text/event-stream" \
|
||||
-d "Plan a 7-day trip to Tokyo, Japan."
|
||||
```
|
||||
|
||||
This returns SSE-formatted events with `id`, `event`, and `data` fields:
|
||||
|
||||
```text
|
||||
id: 1734567890123-0
|
||||
event: message
|
||||
data: # 7-Day Tokyo Adventure
|
||||
|
||||
id: 1734567890124-0
|
||||
event: message
|
||||
data: ## Day 1: Arrival and Exploration
|
||||
|
||||
id: 1734567890999-0
|
||||
event: done
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
The `id` field is the Redis stream entry ID - use it as the `cursor` parameter to resume from that exact point.
|
||||
|
||||
### Understanding the Response Headers
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
| `x-conversation-id` | The conversation ID (session key). Use this to resume the stream. |
|
||||
| `Content-Type` | Either `text/plain` or `text/event-stream` depending on your `Accept` header. |
|
||||
| `Cache-Control` | Set to `no-cache` to prevent caching of the stream. |
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```text
|
||||
┌─────────────┐ POST /agent/create ┌─────────────────────┐
|
||||
│ Client │ (Accept: text/plain or SSE)│ Azure Functions │
|
||||
│ (curl) │ ──────────────────────────► │ (FunctionTriggers) │
|
||||
└─────────────┘ └──────────┬──────────┘
|
||||
▲ │
|
||||
│ Text or SSE stream Signal Entity
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌─────────────────────┐
|
||||
│ │ AgentEntity │
|
||||
│ │ (Durable Entity) │
|
||||
│ └──────────┬──────────┘
|
||||
│ │
|
||||
│ IAgentResponseHandler
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌─────────────────────┐
|
||||
│ │ RedisStreamResponse │
|
||||
│ │ Handler │
|
||||
│ └──────────┬──────────┘
|
||||
│ │
|
||||
│ XADD (write)
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌─────────────────────┐
|
||||
└─────────── XREAD (poll) ────────── │ Redis Streams │
|
||||
│ (Durable Log) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Client sends prompt**: The `Create` endpoint receives the prompt and generates a new agent thread.
|
||||
|
||||
2. **Agent invoked**: The durable entity (`AgentEntity`) is signaled to run the travel planner agent. This is fire-and-forget from the HTTP request's perspective.
|
||||
|
||||
3. **Responses captured**: As the agent generates responses, `RedisStreamResponseHandler` (implementing `IAgentResponseHandler`) extracts the text from each `AgentRunResponseUpdate` and publishes it to a Redis Stream keyed by session ID.
|
||||
|
||||
4. **Client polls Redis**: The HTTP response streams events by polling the Redis Stream. For SSE format, each event includes the Redis entry ID as the `id` field.
|
||||
|
||||
5. **Resumption**: If the client disconnects, it can call the `Stream` endpoint with the conversation ID (from the `x-conversation-id` header) and optionally the last received cursor to resume from that point.
|
||||
|
||||
## Message Delivery Guarantees
|
||||
|
||||
This sample provides **at-least-once delivery** with the following characteristics:
|
||||
|
||||
- **Durability**: Messages are persisted to Redis Streams with configurable TTL (default: 10 minutes).
|
||||
- **Ordering**: Messages are delivered in order within a session.
|
||||
- **Resumption**: Clients can resume from any point using cursor-based pagination.
|
||||
- **Replay**: Clients can replay the entire stream by omitting the cursor.
|
||||
|
||||
### Important Considerations
|
||||
|
||||
- **No exactly-once delivery**: If a client disconnects exactly when receiving a message, it may receive that message again upon resumption. Clients should handle duplicate messages idempotently.
|
||||
- **TTL expiration**: Streams expire after the configured TTL. Clients cannot resume streams that have expired.
|
||||
- **Redis guarantees**: Redis streams are backed by Redis persistence mechanisms (RDB/AOF). Ensure your Redis instance is configured for durability as needed.
|
||||
|
||||
## When to Use These Patterns
|
||||
|
||||
The patterns demonstrated in this sample are ideal for:
|
||||
|
||||
- **Long-running agent tasks**: When agent responses take minutes to complete (e.g., deep research, complex planning)
|
||||
- **Unreliable network connections**: Mobile apps, unstable WiFi, or connections that may drop
|
||||
- **Resumable experiences**: Users should be able to close and reopen an app without losing context
|
||||
- **Background processing**: When you want to fire off a task and check on it later
|
||||
|
||||
These patterns may be overkill for:
|
||||
|
||||
- **Simple, fast responses**: If responses complete in a few seconds, standard streaming is simpler
|
||||
- **Stateless interactions**: If there's no need to resume or replay conversations
|
||||
- **Very high throughput**: Redis adds latency; for maximum throughput, direct streaming may be better
|
||||
|
||||
## Configuration
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|---------------------|-------------|---------|
|
||||
| `REDIS_CONNECTION_STRING` | Redis connection string | `localhost:6379` |
|
||||
| `REDIS_STREAM_TTL_MINUTES` | How long streams are retained after last write | `10` |
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL | (required) |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Azure OpenAI deployment name | (required) |
|
||||
| `AZURE_OPENAI_KEY` | API key (optional, uses Azure CLI auth if not set) | (optional) |
|
||||
|
||||
## Cleanup
|
||||
|
||||
To stop and remove the Redis Docker containers:
|
||||
|
||||
```bash
|
||||
docker stop redis
|
||||
docker rm redis
|
||||
```
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> ⚠️ **This sample is for illustration purposes only and is not intended to be production-ready.**
|
||||
>
|
||||
> A production implementation should consider:
|
||||
>
|
||||
> - Redis cluster configuration for high availability
|
||||
> - Authentication and authorization for the streaming endpoints
|
||||
> - Rate limiting and abuse prevention
|
||||
> - Monitoring and alerting for stream health
|
||||
> - Graceful handling of Redis failures
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace ReliableStreaming;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chunk of data read from a Redis stream.
|
||||
/// </summary>
|
||||
/// <param name="EntryId">The Redis stream entry ID (can be used as a cursor for resumption).</param>
|
||||
/// <param name="Text">The text content of the chunk, or null if this is a completion/error marker.</param>
|
||||
/// <param name="IsDone">True if this chunk marks the end of the stream.</param>
|
||||
/// <param name="Error">An error message if something went wrong, or null otherwise.</param>
|
||||
public readonly record struct StreamChunk(string EntryId, string? Text, bool IsDone, string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IAgentResponseHandler"/> that publishes agent response updates
|
||||
/// to Redis Streams for reliable delivery. This enables clients to disconnect and reconnect
|
||||
/// to ongoing agent responses without losing messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Redis Streams provide a durable, append-only log that supports consumer groups and message
|
||||
/// acknowledgment. This implementation uses auto-generated IDs (which are timestamp-based)
|
||||
/// as sequence numbers, allowing clients to resume from any point in the stream.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each agent session gets its own Redis Stream, keyed by session ID. The stream entries
|
||||
/// contain text chunks extracted from <see cref="AgentRunResponseUpdate"/> objects.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class RedisStreamResponseHandler : IAgentResponseHandler
|
||||
{
|
||||
private const int MaxEmptyReads = 300; // 5 minutes at 1 second intervals
|
||||
private const int PollIntervalMs = 1000;
|
||||
|
||||
private readonly IConnectionMultiplexer _redis;
|
||||
private readonly TimeSpan _streamTtl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RedisStreamResponseHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="redis">The Redis connection multiplexer.</param>
|
||||
/// <param name="streamTtl">The time-to-live for stream entries. Streams will expire after this duration of inactivity.</param>
|
||||
public RedisStreamResponseHandler(IConnectionMultiplexer redis, TimeSpan streamTtl)
|
||||
{
|
||||
this._redis = redis;
|
||||
this._streamTtl = streamTtl;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask OnStreamingResponseUpdateAsync(
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> messageStream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the current session ID from the DurableAgentContext
|
||||
// This is set by the AgentEntity before invoking the response handler
|
||||
DurableAgentContext? context = DurableAgentContext.Current;
|
||||
if (context is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
|
||||
}
|
||||
|
||||
// Get conversation ID from the current thread context, which is only available in the context of
|
||||
// a durable agent execution.
|
||||
string conversationId = context.CurrentThread.GetService<AgentThreadMetadata>()?.ConversationId
|
||||
?? throw new InvalidOperationException("Unable to determine conversation ID from the current thread.");
|
||||
string streamKey = GetStreamKey(conversationId);
|
||||
|
||||
IDatabase db = this._redis.GetDatabase();
|
||||
int sequenceNumber = 0;
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in messageStream.WithCancellation(cancellationToken))
|
||||
{
|
||||
// Extract just the text content - this avoids serialization round-trip issues
|
||||
string text = update.Text;
|
||||
|
||||
// Only publish non-empty text chunks
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
// Create the stream entry with the text and metadata
|
||||
NameValueEntry[] entries =
|
||||
[
|
||||
new NameValueEntry("text", text),
|
||||
new NameValueEntry("sequence", sequenceNumber++),
|
||||
new NameValueEntry("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
|
||||
];
|
||||
|
||||
// Add to the Redis Stream with auto-generated ID (timestamp-based)
|
||||
await db.StreamAddAsync(streamKey, entries);
|
||||
|
||||
// Refresh the TTL on each write to keep the stream alive during active streaming
|
||||
await db.KeyExpireAsync(streamKey, this._streamTtl);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a sentinel entry to mark the end of the stream
|
||||
NameValueEntry[] endEntries =
|
||||
[
|
||||
new NameValueEntry("text", ""),
|
||||
new NameValueEntry("sequence", sequenceNumber),
|
||||
new NameValueEntry("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
|
||||
new NameValueEntry("done", "true"),
|
||||
];
|
||||
await db.StreamAddAsync(streamKey, endEntries);
|
||||
|
||||
// Set final TTL - the stream will be cleaned up after this duration
|
||||
await db.KeyExpireAsync(streamKey, this._streamTtl);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask OnAgentResponseAsync(AgentRunResponse message, CancellationToken cancellationToken)
|
||||
{
|
||||
// This handler is optimized for streaming responses.
|
||||
// For non-streaming responses, we don't need to store in Redis since
|
||||
// the response is returned directly to the caller.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads chunks from a Redis stream for the given session, yielding them as they become available.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The conversation ID to read from.</param>
|
||||
/// <param name="cursor">Optional cursor to resume from. If null, reads from the beginning.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An async enumerable of stream chunks.</returns>
|
||||
public async IAsyncEnumerable<StreamChunk> ReadStreamAsync(
|
||||
string conversationId,
|
||||
string? cursor,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
string streamKey = GetStreamKey(conversationId);
|
||||
|
||||
IDatabase db = this._redis.GetDatabase();
|
||||
string startId = string.IsNullOrEmpty(cursor) ? "0-0" : cursor;
|
||||
|
||||
int emptyReadCount = 0;
|
||||
bool hasSeenData = false;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
StreamEntry[]? entries = null;
|
||||
string? errorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
entries = await db.StreamReadAsync(streamKey, startId, count: 100);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
}
|
||||
|
||||
if (errorMessage != null)
|
||||
{
|
||||
yield return new StreamChunk(startId, null, false, errorMessage);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// entries is guaranteed to be non-null if errorMessage is null
|
||||
if (entries!.Length == 0)
|
||||
{
|
||||
if (!hasSeenData)
|
||||
{
|
||||
emptyReadCount++;
|
||||
if (emptyReadCount >= MaxEmptyReads)
|
||||
{
|
||||
yield return new StreamChunk(
|
||||
startId,
|
||||
null,
|
||||
false,
|
||||
$"Stream not found or timed out after {MaxEmptyReads * PollIntervalMs / 1000} seconds");
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(PollIntervalMs, cancellationToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
hasSeenData = true;
|
||||
|
||||
foreach (StreamEntry entry in entries)
|
||||
{
|
||||
startId = entry.Id.ToString();
|
||||
string? text = entry["text"];
|
||||
string? done = entry["done"];
|
||||
|
||||
if (done == "true")
|
||||
{
|
||||
yield return new StreamChunk(startId, null, true, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
yield return new StreamChunk(startId, text, false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Redis Stream key for a given conversation ID.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The conversation ID.</param>
|
||||
/// <returns>The Redis Stream key.</returns>
|
||||
internal static string GetStreamKey(string conversationId) => $"agent-stream:{conversationId}";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace ReliableStreaming;
|
||||
|
||||
/// <summary>
|
||||
/// Mock travel tools that return hardcoded data for demonstration purposes.
|
||||
/// In a real application, these would call actual weather and events APIs.
|
||||
/// </summary>
|
||||
internal static class TravelTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a weather forecast for a destination on a specific date.
|
||||
/// Returns mock weather data for demonstration purposes.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination city or location.</param>
|
||||
/// <param name="date">The date for the forecast (e.g., "2025-01-15" or "next Monday").</param>
|
||||
/// <returns>A weather forecast summary.</returns>
|
||||
[Description("Gets the weather forecast for a destination on a specific date. Use this to provide weather-aware recommendations in the itinerary.")]
|
||||
public static string GetWeatherForecast(string destination, string date)
|
||||
{
|
||||
// Mock weather data based on destination for realistic responses
|
||||
Dictionary<string, (string condition, int highF, int lowF)> weatherByRegion = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Tokyo"] = ("Partly cloudy with a chance of light rain", 58, 45),
|
||||
["Paris"] = ("Overcast with occasional drizzle", 52, 41),
|
||||
["New York"] = ("Clear and cold", 42, 28),
|
||||
["London"] = ("Foggy morning, clearing in afternoon", 48, 38),
|
||||
["Sydney"] = ("Sunny and warm", 82, 68),
|
||||
["Rome"] = ("Sunny with light breeze", 62, 48),
|
||||
["Barcelona"] = ("Partly sunny", 59, 47),
|
||||
["Amsterdam"] = ("Cloudy with light rain", 46, 38),
|
||||
["Dubai"] = ("Sunny and hot", 85, 72),
|
||||
["Singapore"] = ("Tropical thunderstorms in afternoon", 88, 77),
|
||||
["Bangkok"] = ("Hot and humid, afternoon showers", 91, 78),
|
||||
["Los Angeles"] = ("Sunny and pleasant", 72, 55),
|
||||
["San Francisco"] = ("Morning fog, afternoon sun", 62, 52),
|
||||
["Seattle"] = ("Rainy with breaks", 48, 40),
|
||||
["Miami"] = ("Warm and sunny", 78, 65),
|
||||
["Honolulu"] = ("Tropical paradise weather", 82, 72),
|
||||
};
|
||||
|
||||
// Find a matching destination or use a default
|
||||
(string condition, int highF, int lowF) forecast = ("Partly cloudy", 65, 50);
|
||||
foreach (KeyValuePair<string, (string, int, int)> entry in weatherByRegion)
|
||||
{
|
||||
if (destination.Contains(entry.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
forecast = entry.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $"""
|
||||
Weather forecast for {destination} on {date}:
|
||||
Conditions: {forecast.condition}
|
||||
High: {forecast.highF}°F ({(forecast.highF - 32) * 5 / 9}°C)
|
||||
Low: {forecast.lowF}°F ({(forecast.lowF - 32) * 5 / 9}°C)
|
||||
|
||||
Recommendation: {GetWeatherRecommendation(forecast.condition)}
|
||||
""";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets local events happening at a destination around a specific date.
|
||||
/// Returns mock event data for demonstration purposes.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination city or location.</param>
|
||||
/// <param name="date">The date to search for events (e.g., "2025-01-15" or "next week").</param>
|
||||
/// <returns>A list of local events and activities.</returns>
|
||||
[Description("Gets local events and activities happening at a destination around a specific date. Use this to suggest timely activities and experiences.")]
|
||||
public static string GetLocalEvents(string destination, string date)
|
||||
{
|
||||
// Mock events data based on destination
|
||||
Dictionary<string, string[]> eventsByCity = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Tokyo"] = [
|
||||
"🎭 Kabuki Theater Performance at Kabukiza Theatre - Traditional Japanese drama",
|
||||
"🌸 Winter Illuminations at Yoyogi Park - Spectacular light displays",
|
||||
"🍜 Ramen Festival at Tokyo Station - Sample ramen from across Japan",
|
||||
"🎮 Gaming Expo at Tokyo Big Sight - Latest video games and technology",
|
||||
],
|
||||
["Paris"] = [
|
||||
"🎨 Impressionist Exhibition at Musée d'Orsay - Extended evening hours",
|
||||
"🍷 Wine Tasting Tour in Le Marais - Local sommelier guided",
|
||||
"🎵 Jazz Night at Le Caveau de la Huchette - Historic jazz club",
|
||||
"🥐 French Pastry Workshop - Learn from master pâtissiers",
|
||||
],
|
||||
["New York"] = [
|
||||
"🎭 Broadway Show: Hamilton - Limited engagement performances",
|
||||
"🏀 Knicks vs Lakers at Madison Square Garden",
|
||||
"🎨 Modern Art Exhibit at MoMA - New installations",
|
||||
"🍕 Pizza Walking Tour of Brooklyn - Artisan pizzerias",
|
||||
],
|
||||
["London"] = [
|
||||
"👑 Royal Collection Exhibition at Buckingham Palace",
|
||||
"🎭 West End Musical: The Phantom of the Opera",
|
||||
"🍺 Craft Beer Festival at Brick Lane",
|
||||
"🎪 Winter Wonderland at Hyde Park - Rides and markets",
|
||||
],
|
||||
["Sydney"] = [
|
||||
"🏄 Pro Surfing Competition at Bondi Beach",
|
||||
"🎵 Opera at Sydney Opera House - La Bohème",
|
||||
"🦘 Wildlife Night Safari at Taronga Zoo",
|
||||
"🍽️ Harbor Dinner Cruise with fireworks",
|
||||
],
|
||||
["Rome"] = [
|
||||
"🏛️ After-Hours Vatican Tour - Skip the crowds",
|
||||
"🍝 Pasta Making Class in Trastevere",
|
||||
"🎵 Classical Concert at Borghese Gallery",
|
||||
"🍷 Wine Tasting in Roman Cellars",
|
||||
],
|
||||
};
|
||||
|
||||
// Find events for the destination or use generic events
|
||||
string[] events = [
|
||||
"🎭 Local theater performance",
|
||||
"🍽️ Food and wine festival",
|
||||
"🎨 Art gallery opening",
|
||||
"🎵 Live music at local venues",
|
||||
];
|
||||
|
||||
foreach (KeyValuePair<string, string[]> entry in eventsByCity)
|
||||
{
|
||||
if (destination.Contains(entry.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
events = entry.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string eventList = string.Join("\n• ", events);
|
||||
return $"""
|
||||
Local events in {destination} around {date}:
|
||||
|
||||
• {eventList}
|
||||
|
||||
💡 Tip: Book popular events in advance as they may sell out quickly!
|
||||
""";
|
||||
}
|
||||
|
||||
private static string GetWeatherRecommendation(string condition)
|
||||
{
|
||||
// Use case-insensitive comparison instead of ToLowerInvariant() to satisfy CA1308
|
||||
return condition switch
|
||||
{
|
||||
string c when c.Contains("rain", StringComparison.OrdinalIgnoreCase) || c.Contains("drizzle", StringComparison.OrdinalIgnoreCase) =>
|
||||
"Bring an umbrella and waterproof jacket. Consider indoor activities for backup.",
|
||||
string c when c.Contains("fog", StringComparison.OrdinalIgnoreCase) =>
|
||||
"Morning visibility may be limited. Plan outdoor sightseeing for afternoon.",
|
||||
string c when c.Contains("cold", StringComparison.OrdinalIgnoreCase) =>
|
||||
"Layer up with warm clothing. Hot drinks and cozy cafés recommended.",
|
||||
string c when c.Contains("hot", StringComparison.OrdinalIgnoreCase) || c.Contains("warm", StringComparison.OrdinalIgnoreCase) =>
|
||||
"Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours.",
|
||||
string c when c.Contains("thunder", StringComparison.OrdinalIgnoreCase) || c.Contains("storm", StringComparison.OrdinalIgnoreCase) =>
|
||||
"Keep an eye on weather updates. Have indoor alternatives ready.",
|
||||
_ => "Pleasant conditions expected. Great day for outdoor exploration!"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information",
|
||||
"ReliableStreaming": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>",
|
||||
"REDIS_CONNECTION_STRING": "localhost:6379",
|
||||
"REDIS_STREAM_TTL_MINUTES": "10"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ This directory contains samples for Azure Functions.
|
||||
- **[05_AgentOrchestration_HITL](05_AgentOrchestration_HITL)**: A sample that demonstrates how to implement a human-in-the-loop workflow using durable orchestration, including external event handling for human approval.
|
||||
- **[06_LongRunningTools](06_LongRunningTools)**: A sample that demonstrates how agents can start and interact with durable orchestrations from tool calls to enable long-running tool scenarios.
|
||||
- **[07_AgentAsMcpTool](07_AgentAsMcpTool)**: A sample that demonstrates how to configure durable AI agents to be accessible as Model Context Protocol (MCP) tools.
|
||||
- **[08_ReliableStreaming](08_ReliableStreaming)**: A sample that demonstrates how to implement reliable streaming for durable agents using Redis Streams, enabling clients to disconnect and reconnect without losing messages.
|
||||
|
||||
## Running the Samples
|
||||
|
||||
|
||||
+3
-3
@@ -22,17 +22,17 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -22,17 +22,17 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -35,18 +35,18 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+3
-3
@@ -17,17 +17,17 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
|
||||
.ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
|
||||
+8
-8
@@ -28,13 +28,13 @@ namespace SampleApp
|
||||
{
|
||||
public override string? Name => "UpperCaseParrotAgent";
|
||||
|
||||
public override AgentThread GetNewThread()
|
||||
public override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
|
||||
=> new CustomAgentThread();
|
||||
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? featureCollection = null)
|
||||
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a thread if the user didn't supply one.
|
||||
thread ??= this.GetNewThread();
|
||||
@@ -45,7 +45,7 @@ namespace SampleApp
|
||||
}
|
||||
|
||||
// Clone the input messages and turn them into response messages with upper case text.
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
|
||||
@@ -58,7 +58,7 @@ namespace SampleApp
|
||||
};
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a thread if the user didn't supply one.
|
||||
thread ??= this.GetNewThread();
|
||||
@@ -69,7 +69,7 @@ namespace SampleApp
|
||||
}
|
||||
|
||||
// Clone the input messages and turn them into response messages with upper case text.
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
|
||||
@@ -79,7 +79,7 @@ namespace SampleApp
|
||||
yield return new AgentRunResponseUpdate
|
||||
{
|
||||
AgentId = this.Id,
|
||||
AuthorName = this.DisplayName,
|
||||
AuthorName = message.AuthorName,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = message.Contents,
|
||||
ResponseId = Guid.NewGuid().ToString("N"),
|
||||
@@ -88,7 +88,7 @@ namespace SampleApp
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string agentName) => messages.Select(x =>
|
||||
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string? agentName) => messages.Select(x =>
|
||||
{
|
||||
// Clone the message and update its author to be the agent.
|
||||
var messageClone = x.Clone();
|
||||
|
||||
@@ -11,7 +11,7 @@ var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
|
||||
|
||||
AIAgent agent = new OpenAIClient(
|
||||
apiKey)
|
||||
.GetOpenAIResponseClient(model)
|
||||
.GetResponsesClient(model)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
|
||||
+2
-2
@@ -11,11 +11,11 @@ var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new I
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5";
|
||||
|
||||
var client = new OpenAIClient(apiKey)
|
||||
.GetOpenAIResponseClient(model)
|
||||
.GetResponsesClient(model)
|
||||
.AsIChatClient().AsBuilder()
|
||||
.ConfigureOptions(o =>
|
||||
{
|
||||
o.RawRepresentationFactory = _ => new ResponseCreationOptions()
|
||||
o.RawRepresentationFactory = _ => new CreateResponseOptions()
|
||||
{
|
||||
ReasoningOptions = new()
|
||||
{
|
||||
|
||||
+4
-4
@@ -87,10 +87,10 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task<AgentRunResponse> RunAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunAsync(messages, thread, options, cancellationToken);
|
||||
protected sealed override Task<AgentRunResponse> RunCoreAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunStreamingAsync(messages, thread, options, cancellationToken);
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
|
||||
}
|
||||
|
||||
+11
-11
@@ -16,13 +16,13 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <summary>
|
||||
/// Initialize an instance of <see cref="OpenAIResponseClientAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="client">Instance of <see cref="OpenAIResponseClient"/></param>
|
||||
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
|
||||
/// <param name="instructions">Optional instructions for the agent.</param>
|
||||
/// <param name="name">Optional name for the agent.</param>
|
||||
/// <param name="description">Optional description for the agent.</param>
|
||||
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
|
||||
public OpenAIResponseClientAgent(
|
||||
OpenAIResponseClient client,
|
||||
ResponsesClient client,
|
||||
string? instructions = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
@@ -39,11 +39,11 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <summary>
|
||||
/// Initialize an instance of <see cref="OpenAIResponseClientAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="client">Instance of <see cref="OpenAIResponseClient"/></param>
|
||||
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
|
||||
/// <param name="options">Options to create the agent.</param>
|
||||
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
|
||||
public OpenAIResponseClientAgent(
|
||||
OpenAIResponseClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
|
||||
ResponsesClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
|
||||
base(new ChatClientAgent((client ?? throw new ArgumentNullException(nameof(client))).AsIChatClient(), options, loggerFactory))
|
||||
{
|
||||
}
|
||||
@@ -55,8 +55,8 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="OpenAIResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public virtual async Task<OpenAIResponse> RunAsync(
|
||||
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public virtual async Task<ResponseResult> RunAsync(
|
||||
IEnumerable<ResponseItem> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -74,7 +74,7 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="OpenAIResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
public virtual async IAsyncEnumerable<StreamingResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ResponseItem> messages,
|
||||
AgentThread? thread = null,
|
||||
@@ -105,10 +105,10 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunAsync(messages, thread, options, cancellationToken);
|
||||
protected sealed override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunStreamingAsync(messages, thread, options, cancellationToken);
|
||||
protected sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an OpenAIResponseClient instance.
|
||||
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an ResponsesClient instance.
|
||||
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
@@ -9,16 +9,16 @@ using OpenAIResponseClientSample;
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
|
||||
|
||||
// Create an OpenAIResponseClient directly from OpenAIClient
|
||||
OpenAIResponseClient responseClient = new OpenAIClient(apiKey).GetOpenAIResponseClient(model);
|
||||
// Create a ResponsesClient directly from OpenAIClient
|
||||
ResponsesClient responseClient = new OpenAIClient(apiKey).GetResponsesClient(model);
|
||||
|
||||
// Create an agent directly from the OpenAIResponseClient using OpenAIResponseClientAgent
|
||||
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
|
||||
OpenAIResponseClientAgent agent = new(responseClient, instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
ResponseItem userMessage = ResponseItem.CreateUserMessageItem("Tell me a joke about a pirate.");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
OpenAIResponse response = await agent.RunAsync([userMessage]);
|
||||
ResponseResult response = await agent.RunAsync([userMessage]);
|
||||
Console.WriteLine(response.GetOutputText());
|
||||
|
||||
// Invoke the agent with streaming support.
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to maintain conversation state using the OpenAIResponseClientAgent
|
||||
// and AgentThread. By passing the same thread to multiple agent invocations, the agent
|
||||
// automatically maintains the conversation history, allowing the AI model to understand
|
||||
// context from previous exchanges.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using OpenAI.Conversations;
|
||||
|
||||
string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
||||
string model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
|
||||
|
||||
// Create a ConversationClient directly from OpenAIClient
|
||||
OpenAIClient openAIClient = new(apiKey);
|
||||
ConversationClient conversationClient = openAIClient.GetConversationClient();
|
||||
|
||||
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
|
||||
ChatClientAgent agent = new(openAIClient.GetResponsesClient(model).AsIChatClient(), instructions: "You are a helpful assistant.", name: "ConversationAgent");
|
||||
|
||||
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
|
||||
|
||||
using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createConversationResult.GetRawResponse().Content.ToString());
|
||||
string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!;
|
||||
|
||||
// Create a thread for the conversation - this enables conversation state management for subsequent turns
|
||||
AgentThread thread = agent.GetNewThread(conversationId);
|
||||
|
||||
Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
|
||||
|
||||
// First turn: Ask about a topic
|
||||
Console.WriteLine("User: What is the capital of France?");
|
||||
UserChatMessage firstMessage = new("What is the capital of France?");
|
||||
|
||||
// After this call, the conversation state associated in the options is stored in 'thread' and used in subsequent calls
|
||||
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread);
|
||||
Console.WriteLine($"Assistant: {firstResponse.Content.Last().Text}\n");
|
||||
|
||||
// Second turn: Follow-up question that relies on conversation context
|
||||
Console.WriteLine("User: What famous landmarks are located there?");
|
||||
UserChatMessage secondMessage = new("What famous landmarks are located there?");
|
||||
|
||||
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
|
||||
Console.WriteLine($"Assistant: {secondResponse.Content.Last().Text}\n");
|
||||
|
||||
// Third turn: Another follow-up that demonstrates context continuity
|
||||
Console.WriteLine("User: How tall is the most famous one?");
|
||||
UserChatMessage thirdMessage = new("How tall is the most famous one?");
|
||||
|
||||
ChatCompletion thirdResponse = await agent.RunAsync([thirdMessage], thread);
|
||||
Console.WriteLine($"Assistant: {thirdResponse.Content.Last().Text}\n");
|
||||
|
||||
Console.WriteLine("=== End of Conversation ===");
|
||||
|
||||
// Show full conversation history
|
||||
Console.WriteLine("Full Conversation History:");
|
||||
ClientResult getConversationResult = await conversationClient.GetConversationAsync(conversationId);
|
||||
|
||||
Console.WriteLine("Conversation created.");
|
||||
Console.WriteLine($" Conversation ID: {conversationId}");
|
||||
Console.WriteLine();
|
||||
|
||||
CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId);
|
||||
foreach (ClientResult result in getConversationItemsResults.GetRawPages())
|
||||
{
|
||||
Console.WriteLine("Message contents retrieved. Order is most recent first by default.");
|
||||
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
|
||||
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
|
||||
{
|
||||
string messageId = element.GetProperty("id"u8).ToString();
|
||||
string messageRole = element.GetProperty("role"u8).ToString();
|
||||
Console.WriteLine($" Message ID: {messageId}");
|
||||
Console.WriteLine($" Message Role: {messageRole}");
|
||||
|
||||
foreach (var content in element.GetProperty("content").EnumerateArray())
|
||||
{
|
||||
string messageContentText = content.GetProperty("text"u8).ToString();
|
||||
Console.WriteLine($" Message Text: {messageContentText}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
ClientResult deleteConversationResult = conversationClient.DeleteConversation(conversationId);
|
||||
using JsonDocument deleteConversationResultAsJson = JsonDocument.Parse(deleteConversationResult.GetRawResponse().Content.ToString());
|
||||
bool deleted = deleteConversationResultAsJson.RootElement
|
||||
.GetProperty("deleted"u8)
|
||||
.GetBoolean();
|
||||
|
||||
Console.WriteLine("Conversation deleted.");
|
||||
Console.WriteLine($" Deleted: {deleted}");
|
||||
Console.WriteLine();
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# Managing Conversation State with OpenAI
|
||||
|
||||
This sample demonstrates how to maintain conversation state across multiple turns using the Agent Framework with OpenAI's Conversation API.
|
||||
|
||||
## What This Sample Shows
|
||||
|
||||
- **Conversation State Management**: Shows how to use `ConversationClient` and `AgentThread` to maintain conversation context across multiple agent invocations
|
||||
- **Multi-turn Conversations**: Demonstrates follow-up questions that rely on context from previous messages in the conversation
|
||||
- **Server-Side Storage**: Uses OpenAI's Conversation API to manage conversation history server-side, allowing the model to access previous messages without resending them
|
||||
- **Conversation Lifecycle**: Demonstrates creating, retrieving, and deleting conversations
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### ConversationClient for Server-Side Storage
|
||||
|
||||
The `ConversationClient` manages conversations on OpenAI's servers:
|
||||
|
||||
```csharp
|
||||
// Create a ConversationClient from OpenAIClient
|
||||
OpenAIClient openAIClient = new(apiKey);
|
||||
ConversationClient conversationClient = openAIClient.GetConversationClient();
|
||||
|
||||
// Create a new conversation
|
||||
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
|
||||
```
|
||||
|
||||
### AgentThread for Conversation State
|
||||
|
||||
The `AgentThread` works with `ChatClientAgentRunOptions` to link the agent to a server-side conversation:
|
||||
|
||||
```csharp
|
||||
// Set up agent run options with the conversation ID
|
||||
ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } };
|
||||
|
||||
// Create a thread for the conversation
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
// First call links the thread to the conversation
|
||||
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions);
|
||||
|
||||
// Subsequent calls use the thread without needing to pass options again
|
||||
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
|
||||
```
|
||||
|
||||
### Retrieving Conversation History
|
||||
|
||||
You can retrieve the full conversation history from the server:
|
||||
|
||||
```csharp
|
||||
CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId);
|
||||
foreach (ClientResult result in getConversationItemsResults.GetRawPages())
|
||||
{
|
||||
// Process conversation items
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
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 Thread**: Call `agent.GetNewThread()` to create a new conversation thread
|
||||
5. **Link Thread to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call
|
||||
6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the thread - context is maintained
|
||||
7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()`
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. Set the required environment variables:
|
||||
```powershell
|
||||
$env:OPENAI_API_KEY = "your_api_key_here"
|
||||
$env:OPENAI_MODEL = "gpt-4o-mini"
|
||||
```
|
||||
|
||||
2. Run the sample:
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
The sample demonstrates a three-turn conversation where each follow-up question relies on context from previous messages:
|
||||
|
||||
1. First question asks about the capital of France
|
||||
2. Second question asks about landmarks "there" - requiring understanding of the previous answer
|
||||
3. Third question asks about "the most famous one" - requiring context from both previous turns
|
||||
|
||||
After the conversation, the sample retrieves and displays the full conversation history from the server, then cleans up by deleting the conversation.
|
||||
|
||||
This demonstrates that the conversation state is properly maintained across multiple agent invocations using OpenAI's server-side conversation storage.
|
||||
@@ -13,4 +13,5 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|
||||
|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent with native OpenAI SDK types. Shows both regular and streaming invocation of the agent.|
|
||||
|[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.|
|
||||
|[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.|
|
||||
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|
||||
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|
||||
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentThread for context continuity.|
|
||||
+199
-45
@@ -22,53 +22,200 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
// Replace this with a vector store implementation of your choice if you want to persist the chat history to disk.
|
||||
VectorStore vectorStore = new InMemoryVectorStore();
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
ChatMessageStoreFactory = ctx =>
|
||||
// Execute various samples showing how to use a custom ChatMessageStore with an agent.
|
||||
await CustomChatMessageStore_UsingFactory_Async();
|
||||
await CustomChatMessageStore_UsingFactoryAndExistingExternalId_Async();
|
||||
await CustomChatMessageStore_PerThread_Async();
|
||||
await CustomChatMessageStore_PerRun_Async();
|
||||
|
||||
// Here we can see how to create a custom ChatMessageStore using a factory method
|
||||
// provided to the agent via the ChatMessageStoreFactory option.
|
||||
// This allows us to use a custom chat message store, where the consumer of the agent
|
||||
// doesn't need to know anything about the storage mechanism used.
|
||||
async Task CustomChatMessageStore_UsingFactory_Async()
|
||||
{
|
||||
Console.WriteLine("\n--- With Factory ---\n");
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
// Use a service that doesn't require storage of chat history in the service itself.
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
// Create a new chat message store for this agent that stores the messages in a vector store.
|
||||
// Each thread must get its own copy of the VectorChatMessageStore, since the store
|
||||
// also contains the id that the thread is stored under.
|
||||
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);
|
||||
}
|
||||
});
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
ChatMessageStoreFactory = ctx =>
|
||||
{
|
||||
// Create a new chat message store for this agent that stores the messages in a vector store.
|
||||
// Each thread must get its own copy of the VectorChatMessageStore, since the store
|
||||
// also contains the id that the thread is stored under.
|
||||
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions, ctx.Features);
|
||||
}
|
||||
});
|
||||
|
||||
// Start a new thread for the agent conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
// Start a new thread for the agent conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
// Run the agent with the thread that stores conversation history in the vector store.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
|
||||
// Run the agent with the thread that stores conversation history in the vector store.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
|
||||
|
||||
// Serialize the thread state, so it can be stored for later use.
|
||||
// Since the chat history is stored in the vector store, the serialized thread
|
||||
// only contains the guid that the messages are stored under in the vector store.
|
||||
JsonElement serializedThread = thread.Serialize();
|
||||
// Serialize the thread state, so it can be stored for later use.
|
||||
// Since the chat history is stored in the vector store, the serialized thread
|
||||
// only contains the guid that the messages are stored under in the vector store.
|
||||
JsonElement serializedThread = thread.Serialize();
|
||||
|
||||
Console.WriteLine("\n--- Serialized thread ---\n");
|
||||
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
|
||||
Console.WriteLine("\n--- Serialized thread ---\n");
|
||||
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
// The serialized thread can now be saved to a database, file, or any other storage mechanism
|
||||
// and loaded again later.
|
||||
// The serialized thread can now be saved to a database, file, or any other storage mechanism
|
||||
// and loaded again later.
|
||||
|
||||
// Deserialize the thread state after loading from storage.
|
||||
AgentThread resumedThread = agent.DeserializeThread(serializedThread);
|
||||
// Deserialize the thread state after loading from storage.
|
||||
AgentThread resumedThread = agent.DeserializeThread(serializedThread);
|
||||
|
||||
// Run the agent with the thread that stores conversation 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.", resumedThread));
|
||||
// Run the agent with the thread that stores conversation 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.", resumedThread));
|
||||
}
|
||||
|
||||
// We can access the VectorChatMessageStore via the thread's GetService method if we need to read the key under which threads are stored.
|
||||
var messageStore = resumedThread.GetService<VectorChatMessageStore>()!;
|
||||
Console.WriteLine($"\nThread is stored in vector store under key: {messageStore.ThreadDbKey}");
|
||||
// Here we can see how to create a custom ChatMessageStore using a factory method
|
||||
// provided to the agent via the ChatMessageStoreFactory option.
|
||||
// It also shows how we can pass a custom storage id at runtime to the message store using
|
||||
// the VectorChatMessageStoreThreadDbKeyFeature.
|
||||
// Note that not all agents or chat message stores may support this feature.
|
||||
async Task CustomChatMessageStore_UsingFactoryAndExistingExternalId_Async()
|
||||
{
|
||||
Console.WriteLine("\n--- With Factory and Existing External ID ---\n");
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
// Use a service that doesn't require storage of chat history in the service itself.
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
ChatMessageStoreFactory = ctx =>
|
||||
{
|
||||
// Create a new chat message store for this agent that stores the messages in a vector store.
|
||||
// Each thread must get its own copy of the VectorChatMessageStore, since the store
|
||||
// also contains the id that the thread is stored under.
|
||||
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions, ctx.Features);
|
||||
}
|
||||
});
|
||||
|
||||
// Start a new thread for the agent conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
// Run the agent with the thread that stores conversation history in the vector store.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
|
||||
|
||||
// We can access the VectorChatMessageStore via the thread's GetService method if we need to read the key under which threads are stored.
|
||||
var messageStoreFromFactory = thread.GetService<VectorChatMessageStore>()!;
|
||||
Console.WriteLine($"\nThread is stored in vector store under key: {messageStoreFromFactory.ThreadDbKey}");
|
||||
|
||||
// It's possible to create a new thread that uses the same chat message store id by providing
|
||||
// the VectorChatMessageStoreThreadDbKeyFeature in the feature collection when creating the new thread.
|
||||
AgentThread resumedThread = agent.GetNewThread(
|
||||
new AgentFeatureCollection().WithFeature(new VectorChatMessageStoreThreadDbKeyFeature(messageStoreFromFactory.ThreadDbKey!)));
|
||||
|
||||
// Run the agent with the thread that stores conversation history in the vector store.
|
||||
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
|
||||
}
|
||||
|
||||
// Here we can see how to create a custom ChatMessageStore and pass it to the thread
|
||||
// when creating a new thread.
|
||||
async Task CustomChatMessageStore_PerThread_Async()
|
||||
{
|
||||
Console.WriteLine("\n--- Per Thread ---\n");
|
||||
|
||||
// We can also create an agent without a factory that provides a ChatMessageStore.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
// Use a service that doesn't require storage of chat history in the service itself.
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker"
|
||||
});
|
||||
|
||||
// Instead of using a factory on the agent to create the ChatMessageStore, we can
|
||||
// create a VectorChatMessageStore ourselves and register it in a feature collection.
|
||||
// We can then pass the feature collection when creating a new thread.
|
||||
// We also have the opportunity here to pass any id that we want for storing the chat history in the vector store.
|
||||
VectorChatMessageStore perThreadMessageStore = new(vectorStore, "chat-history-1");
|
||||
AgentThread thread = agent.GetNewThread(new AgentFeatureCollection().WithFeature<ChatMessageStore>(perThreadMessageStore));
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
|
||||
|
||||
// When serializing this thread, we'll see that it has the id from the message store stored in its state.
|
||||
JsonElement serializedThread = thread.Serialize();
|
||||
|
||||
Console.WriteLine("\n--- Serialized thread ---\n");
|
||||
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
// Here we can see how to create a custom ChatMessageStore for a single run using the Features option
|
||||
// passed when we run the agent.
|
||||
// Note that if the agent doesn't support a chat message store, it would be ignored.
|
||||
async Task CustomChatMessageStore_PerRun_Async()
|
||||
{
|
||||
Console.WriteLine("\n--- Per Run ---\n");
|
||||
|
||||
// We can also create an agent without a factory that provides a ChatMessageStore.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
// Use a service that doesn't require storage of chat history in the service itself.
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker"
|
||||
});
|
||||
|
||||
// Start a new thread for the agent conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
// Instead of using a factory on the agent to create the ChatMessageStore, we can
|
||||
// create a VectorChatMessageStore ourselves and register it in a feature collection.
|
||||
// We can then pass the feature collection to the agent when running it by using the Features option.
|
||||
// The message store would only be used for the run that it's passed to.
|
||||
// If the agent doesn't support a message store, it would be ignored.
|
||||
// We also have the opportunity here to pass any id that we want for storing the chat history in the vector store.
|
||||
VectorChatMessageStore perRunMessageStore = new(vectorStore, "chat-history-1");
|
||||
Console.WriteLine(await agent.RunAsync(
|
||||
"Tell me a joke about a pirate.",
|
||||
thread,
|
||||
options: new AgentRunOptions()
|
||||
{
|
||||
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(perRunMessageStore)
|
||||
}));
|
||||
|
||||
// When serializing this thread, we'll see that it has no messagestore state, since the messagestore was not attached to the thread,
|
||||
// but just provided for the single run. Note that, depending on the circumstances, the thread may still contain other state, e.g. Memories,
|
||||
// if an AIContextProvider is attached which adds memory to an agent.
|
||||
JsonElement serializedThread = thread.Serialize();
|
||||
|
||||
Console.WriteLine("\n--- Serialized thread ---\n");
|
||||
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
namespace SampleApp
|
||||
{
|
||||
/// <summary>
|
||||
/// A feature that allows providing the thread database key for the <see cref="VectorChatMessageStore"/>.
|
||||
/// </summary>
|
||||
internal sealed class VectorChatMessageStoreThreadDbKeyFeature(string threadDbKey)
|
||||
{
|
||||
public string ThreadDbKey { get; } = threadDbKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A sample implementation of <see cref="ChatMessageStore"/> that stores chat messages in a vector store.
|
||||
/// </summary>
|
||||
@@ -76,29 +223,36 @@ namespace SampleApp
|
||||
{
|
||||
private readonly VectorStore _vectorStore;
|
||||
|
||||
public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public VectorChatMessageStore(VectorStore vectorStore, string threadDbKey)
|
||||
{
|
||||
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
|
||||
this.ThreadDbKey = threadDbKey ?? throw new ArgumentNullException(nameof(threadDbKey));
|
||||
}
|
||||
|
||||
public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? features = null)
|
||||
{
|
||||
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
|
||||
|
||||
if (serializedStoreState.ValueKind is JsonValueKind.String)
|
||||
{
|
||||
// Here we can deserialize the thread id so that we can access the same messages as before the suspension.
|
||||
this.ThreadDbKey = serializedStoreState.Deserialize<string>();
|
||||
}
|
||||
// Here we can deserialize the thread id so that we can access the same messages as before the suspension, or if
|
||||
// a user provided a ConversationIdAgentFeature in the features collection, we can use that
|
||||
// or finally we can generate one ourselves.
|
||||
this.ThreadDbKey = serializedStoreState.ValueKind is JsonValueKind.String
|
||||
? serializedStoreState.Deserialize<string>()
|
||||
: features?.TryGet<VectorChatMessageStoreThreadDbKeyFeature>(out var threadDbKeyFeature) is true
|
||||
? threadDbKeyFeature.ThreadDbKey
|
||||
: Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
public string? ThreadDbKey { get; private set; }
|
||||
public string? ThreadDbKey { get; }
|
||||
|
||||
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
|
||||
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem()
|
||||
{
|
||||
Key = this.ThreadDbKey + x.MessageId,
|
||||
Key = this.ThreadDbKey + (string.IsNullOrWhiteSpace(x.MessageId) ? Guid.NewGuid().ToString("N") : x.MessageId),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
ThreadId = this.ThreadDbKey,
|
||||
SerializedMessage = JsonSerializer.Serialize(x),
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ var stateStore = new Dictionary<string, JsonElement?>();
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
name: "SpaceNovelWriter",
|
||||
instructions: "You are a space novel writer. Always research relevant facts and generate character profiles for the main characters before writing novels." +
|
||||
|
||||
@@ -13,7 +13,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent();
|
||||
|
||||
// Enable background responses (only supported by OpenAI Responses at this time).
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ internal sealed class Program
|
||||
Dictionary<string, byte[]> screenshots = ComputerUseUtil.LoadScreenshotAssets();
|
||||
|
||||
ChatOptions chatOptions = new();
|
||||
ResponseCreationOptions responseCreationOptions = new()
|
||||
CreateResponseOptions responseCreationOptions = new()
|
||||
{
|
||||
TruncationMode = ResponseTruncationMode.Auto
|
||||
};
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ var mcpTool = new HostedMcpServerTool(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgent",
|
||||
@@ -57,7 +57,7 @@ var mcpToolWithApproval = new HostedMcpServerTool(
|
||||
AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgentWithApproval",
|
||||
|
||||
@@ -36,9 +36,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251125.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -48,9 +48,9 @@ public class WeatherForecastAgent : DelegatingAIAgent
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await base.RunAsync(messages, thread, options, cancellationToken);
|
||||
var response = await base.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
// If the agent returned a valid structured output response
|
||||
// we might be able to enhance the response with an adaptive card.
|
||||
|
||||
@@ -27,7 +27,7 @@ TokenCredential browserCredential = new InteractiveBrowserCredential(
|
||||
using IChatClient client = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.GetResponsesClient(deploymentName)
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.WithPurview(browserCredential, new PurviewSettings("Agent Framework Test App"))
|
||||
|
||||
@@ -30,7 +30,6 @@ internal sealed class A2AAgent : AIAgent
|
||||
private readonly string? _id;
|
||||
private readonly string? _name;
|
||||
private readonly string? _description;
|
||||
private readonly string? _displayName;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -40,9 +39,8 @@ internal sealed class A2AAgent : AIAgent
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The the name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <param name="displayName">The display name of the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
|
||||
public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null)
|
||||
public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
_ = Throw.IfNull(a2aClient);
|
||||
|
||||
@@ -50,13 +48,17 @@ internal sealed class A2AAgent : AIAgent
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._description = description;
|
||||
this._displayName = displayName;
|
||||
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<A2AAgent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override AgentThread GetNewThread()
|
||||
=> new A2AAgentThread();
|
||||
public sealed override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
|
||||
=> new A2AAgentThread()
|
||||
{
|
||||
ContextId = featureCollection?.TryGet<ConversationIdAgentFeature>(out var conversationIdFeature) is true
|
||||
? conversationIdFeature.ConversationId
|
||||
: null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get a new <see cref="AgentThread"/> instance using an existing context id, to continue that conversation.
|
||||
@@ -67,11 +69,11 @@ internal sealed class A2AAgent : AIAgent
|
||||
=> new A2AAgentThread() { ContextId = contextId };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? featureCollection = null)
|
||||
=> new A2AAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
@@ -134,7 +136,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
@@ -198,14 +200,11 @@ internal sealed class A2AAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Id => this._id ?? base.Id;
|
||||
protected override string? IdCore => this._id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Name => this._name ?? base.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => this._displayName ?? base.DisplayName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Description => this._description ?? base.Description;
|
||||
|
||||
|
||||
@@ -33,9 +33,8 @@ public static class A2AClientExtensions
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The the name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <param name="displayName">The display name of the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
|
||||
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
|
||||
public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null) =>
|
||||
new A2AAgent(client, id, name, description, displayName, loggerFactory);
|
||||
public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) =>
|
||||
new A2AAgent(client, id, name, description, loggerFactory);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ namespace Microsoft.Agents.AI;
|
||||
[DebuggerDisplay("{DisplayName,nq}")]
|
||||
public abstract class AIAgent
|
||||
{
|
||||
/// <summary>Default ID of this agent instance.</summary>
|
||||
private readonly string _id = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this agent instance.
|
||||
/// </summary>
|
||||
@@ -37,7 +34,19 @@ public abstract class AIAgent
|
||||
/// agent instances in multi-agent scenarios. They should remain stable for the lifetime
|
||||
/// of the agent instance.
|
||||
/// </remarks>
|
||||
public virtual string Id => this._id;
|
||||
public string Id { get => this.IdCore ?? field; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a custom identifier for the agent, which can be overridden by derived classes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A string representing the agent's identifier, or <see langword="null"/> if the default ID should be used.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Derived classes can override this property to provide a custom identifier.
|
||||
/// When <see langword="null"/> is returned, the <see cref="Id"/> property will use the default randomly-generated identifier.
|
||||
/// </remarks>
|
||||
protected virtual string? IdCore => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the human-readable name of the agent.
|
||||
@@ -51,18 +60,6 @@ public abstract class AIAgent
|
||||
/// </remarks>
|
||||
public virtual string? Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a display-friendly name for the agent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The agent's <see cref="Name"/> if available, otherwise the <see cref="Id"/>.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// This property provides a guaranteed non-null string suitable for display in user interfaces,
|
||||
/// logs, or other contexts where a readable identifier is needed.
|
||||
/// </remarks>
|
||||
public virtual string DisplayName => this.Name ?? this.Id ?? this._id; // final fallback to _id in case Id override returns null
|
||||
|
||||
/// <summary>
|
||||
/// Gets a description of the agent's purpose, capabilities, or behavior.
|
||||
/// </summary>
|
||||
@@ -108,6 +105,7 @@ public abstract class AIAgent
|
||||
/// <summary>
|
||||
/// Creates a new conversation thread that is compatible with this agent.
|
||||
/// </summary>
|
||||
/// <param name="featureCollection">An optional feature collection to override or provide additional context or capabilities to the thread where the thread supports these features.</param>
|
||||
/// <returns>A new <see cref="AgentThread"/> instance ready for use with this agent.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -121,13 +119,14 @@ public abstract class AIAgent
|
||||
/// may be deferred until first use to optimize performance.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract AgentThread GetNewThread();
|
||||
public abstract AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an agent thread from its JSON serialized representation.
|
||||
/// </summary>
|
||||
/// <param name="serializedThread">A <see cref="JsonElement"/> containing the serialized thread state.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
|
||||
/// <param name="featureCollection">An optional feature collection to override or provide additional context or capabilities to the thread where the thread supports these features.</param>
|
||||
/// <returns>A restored <see cref="AgentThread"/> instance with the state from <paramref name="serializedThread"/>.</returns>
|
||||
/// <exception cref="ArgumentException">The <paramref name="serializedThread"/> is not in the expected format.</exception>
|
||||
/// <exception cref="JsonException">The serialized data is invalid or cannot be deserialized.</exception>
|
||||
@@ -136,7 +135,7 @@ public abstract class AIAgent
|
||||
/// allowing conversations to resume across application restarts or be migrated between
|
||||
/// different agent instances.
|
||||
/// </remarks>
|
||||
public abstract AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null);
|
||||
public abstract AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? featureCollection = null);
|
||||
|
||||
/// <summary>
|
||||
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
|
||||
@@ -221,6 +220,35 @@ public abstract class AIAgent
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method delegates to <see cref="RunCoreAsync"/> to perform the actual agent invocation. It handles collections of messages,
|
||||
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
|
||||
/// context-rich conversations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The messages are processed in the order provided and become part of the conversation history.
|
||||
/// The agent's response will also be added to <paramref name="thread"/> if one is provided.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunCoreAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of the agent invocation logic with a collection of chat messages.
|
||||
/// </summary>
|
||||
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
|
||||
/// <param name="thread">
|
||||
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
|
||||
/// The thread will be updated with the input messages and any response messages generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the primary invocation method that implementations must override. It handles collections of messages,
|
||||
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
|
||||
/// context-rich conversations.
|
||||
@@ -230,7 +258,7 @@ public abstract class AIAgent
|
||||
/// The agent's response will also be added to <paramref name="thread"/> if one is provided.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
protected abstract Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -315,6 +343,34 @@ public abstract class AIAgent
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method delegates to <see cref="RunCoreStreamingAsync"/> to perform the actual streaming invocation. It provides real-time
|
||||
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each <see cref="AgentRunResponseUpdate"/> represents a portion of the complete response, allowing consumers
|
||||
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
|
||||
/// </summary>
|
||||
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
|
||||
/// <param name="thread">
|
||||
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
|
||||
/// The thread will be updated with the input messages and any response updates generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the primary streaming invocation method that implementations must override. It provides real-time
|
||||
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
|
||||
/// </para>
|
||||
@@ -323,7 +379,7 @@ public abstract class AIAgent
|
||||
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected abstract IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -34,6 +34,7 @@ public class AgentRunOptions
|
||||
this.ContinuationToken = options.ContinuationToken;
|
||||
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
|
||||
this.AdditionalProperties = options.AdditionalProperties?.Clone();
|
||||
this.Features = options.Features;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,4 +91,9 @@ public class AgentRunOptions
|
||||
/// preserving implementation-specific details or extending the options with custom data.
|
||||
/// </remarks>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of features provided by the caller and middleware for this run.
|
||||
/// </summary>
|
||||
public IAgentFeatureCollection? Features { get; set; }
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ namespace Microsoft.Agents.AI;
|
||||
/// <item><description>Chat history reduction, e.g. where messages needs to be summarized or truncated to reduce the size.</description></item>
|
||||
/// </list>
|
||||
/// An <see cref="AgentThread"/> is always constructed by an <see cref="AIAgent"/> so that the <see cref="AIAgent"/>
|
||||
/// can attach any necessary behaviors to the <see cref="AgentThread"/>. See the <see cref="AIAgent.GetNewThread()"/>
|
||||
/// and <see cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?)"/> methods for more information.
|
||||
/// can attach any necessary behaviors to the <see cref="AgentThread"/>. See the <see cref="AIAgent.GetNewThread(Microsoft.Agents.AI.IAgentFeatureCollection?)"/>
|
||||
/// and <see cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?, Microsoft.Agents.AI.IAgentFeatureCollection?)"/> methods for more information.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because of these behaviors, an <see cref="AgentThread"/> may not be reusable across different agents, since each agent
|
||||
@@ -37,13 +37,13 @@ namespace Microsoft.Agents.AI;
|
||||
/// To support conversations that may need to survive application restarts or separate service requests, an <see cref="AgentThread"/> can be serialized
|
||||
/// and deserialized, so that it can be saved in a persistent store.
|
||||
/// The <see cref="AgentThread"/> provides the <see cref="Serialize(JsonSerializerOptions?)"/> method to serialize the thread to a
|
||||
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?)"/> method
|
||||
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?, Microsoft.Agents.AI.IAgentFeatureCollection?)"/> method
|
||||
/// can be used to deserialize the thread.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="AIAgent"/>
|
||||
/// <seealso cref="AIAgent.GetNewThread()"/>
|
||||
/// <seealso cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?)"/>
|
||||
/// <seealso cref="AIAgent.GetNewThread(Microsoft.Agents.AI.IAgentFeatureCollection?)"/>
|
||||
/// <seealso cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?, Microsoft.Agents.AI.IAgentFeatureCollection?)"/>
|
||||
public abstract class AgentThread
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// Derived classes can override specific methods to add custom behavior while maintaining compatibility with the agent interface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class DelegatingAIAgent : AIAgent
|
||||
public abstract class DelegatingAIAgent : AIAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelegatingAIAgent"/> class with the specified inner agent.
|
||||
@@ -54,7 +54,7 @@ public class DelegatingAIAgent : AIAgent
|
||||
protected AIAgent InnerAgent { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => this.InnerAgent.Id;
|
||||
protected override string? IdCore => this.InnerAgent.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Name => this.InnerAgent.Name;
|
||||
@@ -74,14 +74,14 @@ public class DelegatingAIAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AgentThread GetNewThread() => this.InnerAgent.GetNewThread();
|
||||
public override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null) => this.InnerAgent.GetNewThread(featureCollection);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? featureCollection = null)
|
||||
=> this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions, featureCollection);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -89,7 +89,7 @@ public class DelegatingAIAgent : AIAgent
|
||||
=> this.InnerAgent.RunAsync(messages, thread, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
#pragma warning disable CA1043 // Use Integral Or String Argument For Indexers
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation for <see cref="IAgentFeatureCollection"/>.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Count = {GetCount()}")]
|
||||
[DebuggerTypeProxy(typeof(FeatureCollectionDebugView))]
|
||||
public class AgentFeatureCollection : IAgentFeatureCollection
|
||||
{
|
||||
private readonly IAgentFeatureCollection? _innerCollection;
|
||||
private Dictionary<Type, object>? _features;
|
||||
private volatile int _containerRevision;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AgentFeatureCollection"/>.
|
||||
/// </summary>
|
||||
public AgentFeatureCollection()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AgentFeatureCollection"/> with the specified initial capacity.
|
||||
/// </summary>
|
||||
/// <param name="initialCapacity">The initial number of elements that the collection can contain.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="initialCapacity"/> is less than 0</exception>
|
||||
public AgentFeatureCollection(int initialCapacity)
|
||||
{
|
||||
Throw.IfLessThan(initialCapacity, 0);
|
||||
this._features = new(initialCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AgentFeatureCollection"/> with the specified inner collection.
|
||||
/// </summary>
|
||||
/// <param name="innerCollection">The inner collection.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When providing an inner collection, and if a feature is not found in this collection,
|
||||
/// an attempt will be made to retrieve it from the inner collection as a fallback.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="Remove{TFeature}"/> method will only remove features from this collection
|
||||
/// and not from the inner collection. When removing a feature from this collection, and
|
||||
/// it exists in the inner collection, it will still be retrievable from the inner collection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public AgentFeatureCollection(IAgentFeatureCollection innerCollection)
|
||||
{
|
||||
this._innerCollection = Throw.IfNull(innerCollection);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Revision
|
||||
{
|
||||
get { return this._containerRevision + (this._innerCollection?.Revision ?? 0); }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReadOnly { get { return false; } }
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
|
||||
{
|
||||
if (this._features is not { Count: > 0 })
|
||||
{
|
||||
IEnumerable<KeyValuePair<Type, object>> e = ((IEnumerable<KeyValuePair<Type, object>>?)this._innerCollection) ?? [];
|
||||
return e.GetEnumerator();
|
||||
}
|
||||
|
||||
if (this._innerCollection is null)
|
||||
{
|
||||
return this._features.GetEnumerator();
|
||||
}
|
||||
|
||||
if (this._innerCollection is AgentFeatureCollection innerCollection && innerCollection._features is not { Count: > 0 })
|
||||
{
|
||||
return this._features.GetEnumerator();
|
||||
}
|
||||
|
||||
return YieldAll();
|
||||
|
||||
IEnumerator<KeyValuePair<Type, object>> YieldAll()
|
||||
{
|
||||
HashSet<Type> set = [];
|
||||
|
||||
foreach (var entry in this._features)
|
||||
{
|
||||
set.Add(entry.Key);
|
||||
yield return entry;
|
||||
}
|
||||
|
||||
foreach (var entry in this._innerCollection.Where(x => !set.Contains(x.Key)))
|
||||
{
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGet<TFeature>([MaybeNullWhen(false)] out TFeature feature)
|
||||
where TFeature : notnull
|
||||
{
|
||||
if (this.TryGet(typeof(TFeature), out var obj))
|
||||
{
|
||||
feature = (TFeature)obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
feature = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGet(Type type, [MaybeNullWhen(false)] out object feature)
|
||||
{
|
||||
if (this._features?.TryGetValue(type, out var obj) is true)
|
||||
{
|
||||
feature = obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._innerCollection?.TryGet(type, out var defaultFeature) is true)
|
||||
{
|
||||
feature = defaultFeature;
|
||||
return true;
|
||||
}
|
||||
|
||||
feature = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Set<TFeature>(TFeature instance)
|
||||
where TFeature : notnull
|
||||
{
|
||||
Throw.IfNull(instance);
|
||||
|
||||
this._features ??= new();
|
||||
this._features[typeof(TFeature)] = instance;
|
||||
this._containerRevision++;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove<TFeature>()
|
||||
where TFeature : notnull
|
||||
=> this.Remove(typeof(TFeature));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove(Type type)
|
||||
{
|
||||
if (this._features?.Remove(type) is true)
|
||||
{
|
||||
this._containerRevision++;
|
||||
}
|
||||
}
|
||||
|
||||
// Used by the debugger. Count over enumerable is required to get the correct value.
|
||||
private int GetCount() => this.Count();
|
||||
|
||||
private sealed class FeatureCollectionDebugView(AgentFeatureCollection features)
|
||||
{
|
||||
private readonly AgentFeatureCollection _features = features;
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||
public DictionaryItemDebugView<Type, object>[] Items => this._features.Select(pair => new DictionaryItemDebugView<Type, object>(pair)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a key/value pair for displaying an item of a dictionary by a debugger.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Value}", Name = "[{Key}]")]
|
||||
internal readonly struct DictionaryItemDebugView<TKey, TValue>
|
||||
{
|
||||
public DictionaryItemDebugView(TKey key, TValue value)
|
||||
{
|
||||
this.Key = key;
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public DictionaryItemDebugView(KeyValuePair<TKey, TValue> keyValue)
|
||||
{
|
||||
this.Key = keyValue.Key;
|
||||
this.Value = keyValue.Value;
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
|
||||
public TKey Key { get; }
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
|
||||
public TValue Value { get; }
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IAgentFeatureCollection"/>.
|
||||
/// </summary>
|
||||
public static class AgentFeatureCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the specified feature to the collection and returns the collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFeature">The feature key.</typeparam>
|
||||
/// <param name="features">The feature collection to add the new feature to.</param>
|
||||
/// <param name="feature">The feature to add to the collection.</param>
|
||||
/// <returns>The updated collection.</returns>
|
||||
public static IAgentFeatureCollection WithFeature<TFeature>(this IAgentFeatureCollection features, TFeature feature)
|
||||
where TFeature : notnull
|
||||
{
|
||||
features.Set(feature);
|
||||
return features;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// An agent feature that allows providing a conversation identifier.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This feature allows a user to provide a specific identifier for chat history when stored in the underlying AI service.
|
||||
/// </remarks>
|
||||
public class ConversationIdAgentFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConversationIdAgentFeature"/> class with the specified thread
|
||||
/// identifier.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The unique identifier of the thread required by the underlying AI service. Cannot be <see langword="null"/> or empty.</param>
|
||||
public ConversationIdAgentFeature(string conversationId)
|
||||
{
|
||||
this.ConversationId = Throw.IfNullOrWhitespace(conversationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation identifier.
|
||||
/// </summary>
|
||||
public string ConversationId { get; }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
#pragma warning disable CA1043 // Use Integral Or String Argument For Indexers
|
||||
#pragma warning disable CA1716 // Identifiers should not match keywords
|
||||
|
||||
/// <summary>
|
||||
/// Represents a collection of Agent features.
|
||||
/// </summary>
|
||||
public interface IAgentFeatureCollection : IEnumerable<KeyValuePair<Type, object>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the collection can be modified.
|
||||
/// </summary>
|
||||
bool IsReadOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Incremented for each modification and can be used to verify cached results.
|
||||
/// </summary>
|
||||
int Revision { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a feature of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFeature">The type of the feature to retrieve.</typeparam>
|
||||
/// <param name="feature">When this method returns, contains the feature of type <typeparamref name="TFeature"/> if found; otherwise, the
|
||||
/// default value for the type.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the feature of type <typeparamref name="TFeature"/> was successfully retrieved;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
bool TryGet<TFeature>([MaybeNullWhen(false)] out TFeature feature)
|
||||
where TFeature : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a feature of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the feature to get.</param>
|
||||
/// <param name="feature">When this method returns, contains the feature of type <paramref name="type"/> if found; otherwise, the
|
||||
/// default value for the type.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the feature of type <paramref name="type"/> was successfully retrieved;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
bool TryGet(Type type, [MaybeNullWhen(false)] out object feature);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a feature from the collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFeature">The feature key.</typeparam>
|
||||
void Remove<TFeature>()
|
||||
where TFeature : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Remove a feature from the collection.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the feature to remove.</param>
|
||||
void Remove(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the given feature in the collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFeature">The feature key.</typeparam>
|
||||
/// <param name="instance">The feature value.</param>
|
||||
void Set<TFeature>(TFeature instance)
|
||||
where TFeature : notnull;
|
||||
}
|
||||
@@ -23,11 +23,6 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
private readonly AgentRecord? _agentRecord;
|
||||
private readonly ChatOptions? _chatOptions;
|
||||
private readonly AgentReference _agentReference;
|
||||
/// <summary>
|
||||
/// The usage of a no-op model is a necessary change to avoid OpenAIClients to throw exceptions when
|
||||
/// used with Azure AI Agents as the model used is now defined at the agent creation time.
|
||||
/// </summary>
|
||||
private const string NoOpModel = "no-op";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AzureAIProjectChatClient"/> class.
|
||||
@@ -42,7 +37,7 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentReference agentReference, string? defaultModelId, ChatOptions? chatOptions)
|
||||
: base(Throw.IfNull(aiProjectClient)
|
||||
.GetProjectOpenAIClient()
|
||||
.GetOpenAIResponseClient(defaultModelId ?? NoOpModel)
|
||||
.GetProjectResponsesClientForAgent(agentReference)
|
||||
.AsIChatClient())
|
||||
{
|
||||
this._agentClient = aiProjectClient;
|
||||
@@ -132,13 +127,15 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
|
||||
agentEnabledChatOptions.RawRepresentationFactory = (client) =>
|
||||
{
|
||||
if (originalFactory?.Invoke(this) is not ResponseCreationOptions responseCreationOptions)
|
||||
if (originalFactory?.Invoke(this) is not CreateResponseOptions responseCreationOptions)
|
||||
{
|
||||
responseCreationOptions = new ResponseCreationOptions();
|
||||
responseCreationOptions = new CreateResponseOptions();
|
||||
}
|
||||
|
||||
ResponseCreationOptionsExtensions.set_Agent(responseCreationOptions, this._agentReference);
|
||||
ResponseCreationOptionsExtensions.set_Model(responseCreationOptions, null);
|
||||
responseCreationOptions.Agent = this._agentReference;
|
||||
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
responseCreationOptions.Patch.Remove("$.model"u8);
|
||||
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
return responseCreationOptions;
|
||||
};
|
||||
|
||||
@@ -400,7 +400,7 @@ public static partial class AzureAIProjectChatClientExtensions
|
||||
};
|
||||
|
||||
// Attempt to capture breaking glass options from the raw representation factory that match the agent definition.
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is ResponseCreationOptions respCreationOptions)
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions)
|
||||
{
|
||||
agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions;
|
||||
}
|
||||
@@ -466,7 +466,7 @@ public static partial class AzureAIProjectChatClientExtensions
|
||||
};
|
||||
|
||||
// Attempt to capture breaking glass options from the raw representation factory that match the agent definition.
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is ResponseCreationOptions respCreationOptions)
|
||||
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions)
|
||||
{
|
||||
agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,13 @@ public class CopilotStudioAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override AgentThread GetNewThread()
|
||||
=> new CopilotStudioAgentThread();
|
||||
public sealed override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
|
||||
=> new CopilotStudioAgentThread()
|
||||
{
|
||||
ConversationId = featureCollection?.TryGet<ConversationIdAgentFeature>(out var conversationIdFeature) is true
|
||||
? conversationIdFeature.ConversationId
|
||||
: null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get a new <see cref="AgentThread"/> instance using an existing conversation id, to continue that conversation.
|
||||
@@ -54,11 +59,11 @@ public class CopilotStudioAgent : AIAgent
|
||||
=> new CopilotStudioAgentThread() { ConversationId = conversationId };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? featureCollection = null)
|
||||
=> new CopilotStudioAgentThread(serializedThread, jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -96,7 +101,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -217,9 +217,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a checkpoint document stored in Cosmos DB.
|
||||
/// </summary>
|
||||
/// <summary>Represents a checkpoint document stored in Cosmos DB.</summary>
|
||||
internal sealed class CosmosCheckpointDocument
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
|
||||
@@ -81,7 +81,7 @@ internal sealed partial class DevUIMiddleware
|
||||
}
|
||||
|
||||
context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
|
||||
context.Response.Headers.Location = redirectUrl;
|
||||
context.Response.Headers.Location = redirectUrl; // CodeQL [SM04598] justification: The redirect URL is constructed from a server-configured base path (_basePath), not user input. The query string is only appended as parameters and cannot change the redirect destination since this is a relative URL.
|
||||
|
||||
if (this._logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
|
||||
@@ -231,7 +231,7 @@ internal static class EntitiesApiExtensions
|
||||
return new EntityInfo(
|
||||
Id: entityId,
|
||||
Type: "agent",
|
||||
Name: agent.DisplayName,
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent_framework",
|
||||
Tools: tools,
|
||||
|
||||
@@ -16,29 +16,34 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
|
||||
private readonly DurableTaskClient _client = services.GetRequiredService<DurableTaskClient>();
|
||||
private readonly ILoggerFactory _loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
private readonly IAgentResponseHandler? _messageHandler = services.GetService<IAgentResponseHandler>();
|
||||
private readonly DurableAgentsOptions _options = services.GetRequiredService<DurableAgentsOptions>();
|
||||
private readonly CancellationToken _cancellationToken = cancellationToken != default
|
||||
? cancellationToken
|
||||
: services.GetService<IHostApplicationLifetime>()?.ApplicationStopping ?? CancellationToken.None;
|
||||
|
||||
public async Task<AgentRunResponse> RunAgentAsync(RunRequest request)
|
||||
public Task<AgentRunResponse> RunAgentAsync(RunRequest request)
|
||||
{
|
||||
return this.Run(request);
|
||||
}
|
||||
|
||||
// IDE1006 and VSTHRD200 disabled to allow method name to match the common cross-platform entity operation name.
|
||||
#pragma warning disable IDE1006
|
||||
#pragma warning disable VSTHRD200
|
||||
public async Task<AgentRunResponse> Run(RunRequest request)
|
||||
#pragma warning restore VSTHRD200
|
||||
#pragma warning restore IDE1006
|
||||
{
|
||||
AgentSessionId sessionId = this.Context.Id;
|
||||
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents =
|
||||
this._services.GetRequiredService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>();
|
||||
if (!agents.TryGetValue(sessionId.Name, out Func<IServiceProvider, AIAgent>? agentFactory))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
|
||||
}
|
||||
|
||||
AIAgent agent = agentFactory(this._services);
|
||||
AIAgent agent = this.GetAgent(sessionId);
|
||||
EntityAgentWrapper agentWrapper = new(agent, this.Context, request, this._services);
|
||||
|
||||
// Logger category is Microsoft.DurableTask.Agents.{agentName}.{sessionId}
|
||||
ILogger logger = this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agent.Name}.{sessionId.Key}");
|
||||
ILogger logger = this.GetLogger(agent.Name!, sessionId.Key);
|
||||
|
||||
if (request.Messages.Count == 0)
|
||||
{
|
||||
logger.LogInformation("Ignoring empty request");
|
||||
return new AgentRunResponse();
|
||||
}
|
||||
|
||||
this.State.Data.ConversationHistory.Add(DurableAgentStateRequest.FromRunRequest(request));
|
||||
@@ -113,6 +118,36 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
|
||||
response.Usage?.TotalTokenCount);
|
||||
}
|
||||
|
||||
// Update TTL expiration time. Only schedule deletion check on first interaction.
|
||||
// Subsequent interactions just update the expiration time; CheckAndDeleteIfExpiredAsync
|
||||
// will reschedule the deletion check when it runs.
|
||||
TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
DateTime newExpirationTime = DateTime.UtcNow.Add(timeToLive.Value);
|
||||
bool isFirstInteraction = this.State.Data.ExpirationTimeUtc is null;
|
||||
|
||||
this.State.Data.ExpirationTimeUtc = newExpirationTime;
|
||||
logger.LogTTLExpirationTimeUpdated(sessionId, newExpirationTime);
|
||||
|
||||
// Only schedule deletion check on the first interaction when entity is created.
|
||||
// On subsequent interactions, we just update the expiration time. The scheduled
|
||||
// CheckAndDeleteIfExpiredAsync will reschedule itself if the entity hasn't expired.
|
||||
if (isFirstInteraction)
|
||||
{
|
||||
this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TTL is disabled. Clear the expiration time if it was previously set.
|
||||
if (this.State.Data.ExpirationTimeUtc.HasValue)
|
||||
{
|
||||
logger.LogTTLExpirationTimeCleared(sessionId);
|
||||
this.State.Data.ExpirationTimeUtc = null;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
finally
|
||||
@@ -121,4 +156,78 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
|
||||
DurableAgentContext.ClearCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the entity has expired and deletes it if so, otherwise reschedules the deletion check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is called by the durable task runtime when a <c>CheckAndDeleteIfExpired</c> signal is received.
|
||||
/// </remarks>
|
||||
public void CheckAndDeleteIfExpired()
|
||||
{
|
||||
AgentSessionId sessionId = this.Context.Id;
|
||||
AIAgent agent = this.GetAgent(sessionId);
|
||||
ILogger logger = this.GetLogger(agent.Name!, sessionId.Key);
|
||||
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
DateTime? expirationTime = this.State.Data.ExpirationTimeUtc;
|
||||
|
||||
logger.LogTTLDeletionCheck(sessionId, expirationTime, currentTime);
|
||||
|
||||
if (expirationTime.HasValue)
|
||||
{
|
||||
if (currentTime >= expirationTime.Value)
|
||||
{
|
||||
// Entity has expired, delete it
|
||||
logger.LogTTLEntityExpired(sessionId, expirationTime.Value);
|
||||
this.State = null!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Entity hasn't expired yet, reschedule the deletion check
|
||||
TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleDeletionCheck(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive)
|
||||
{
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
DateTime expirationTime = this.State.Data.ExpirationTimeUtc ?? currentTime.Add(timeToLive);
|
||||
TimeSpan minimumDelay = this._options.MinimumTimeToLiveSignalDelay;
|
||||
|
||||
// To avoid excessive scheduling, we schedule the deletion check for no less than the minimum delay.
|
||||
DateTime scheduledTime = expirationTime > currentTime.Add(minimumDelay)
|
||||
? expirationTime
|
||||
: currentTime.Add(minimumDelay);
|
||||
|
||||
logger.LogTTLDeletionScheduled(sessionId, scheduledTime);
|
||||
|
||||
// Schedule a signal to self to check for expiration
|
||||
this.Context.SignalEntity(
|
||||
this.Context.Id,
|
||||
nameof(CheckAndDeleteIfExpired), // self-signal
|
||||
options: new SignalEntityOptions { SignalTime = scheduledTime });
|
||||
}
|
||||
|
||||
private AIAgent GetAgent(AgentSessionId sessionId)
|
||||
{
|
||||
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents =
|
||||
this._services.GetRequiredService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>();
|
||||
if (!agents.TryGetValue(sessionId.Name, out Func<IServiceProvider, AIAgent>? agentFactory))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
|
||||
}
|
||||
|
||||
return agentFactory(this._services);
|
||||
}
|
||||
|
||||
private ILogger GetLogger(string agentName, string sessionKey)
|
||||
{
|
||||
return this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agentName}.{sessionKey}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Release History
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
|
||||
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
|
||||
|
||||
## v1.0.0-preview.251204.1
|
||||
|
||||
- Added orchestration ID to durable agent entity state ([#2137](https://github.com/microsoft/agent-framework/pull/2137))
|
||||
|
||||
@@ -22,7 +22,7 @@ internal class DefaultDurableAgentClient(DurableTaskClient client, ILoggerFactor
|
||||
|
||||
await this._client.Entities.SignalEntityAsync(
|
||||
sessionId,
|
||||
nameof(AgentEntity.RunAgentAsync),
|
||||
nameof(AgentEntity.Run),
|
||||
request,
|
||||
cancellation: cancellationToken);
|
||||
|
||||
|
||||
@@ -33,21 +33,17 @@ public sealed class DurableAIAgent : AIAgent
|
||||
/// Creates a new agent thread for this agent using a random session ID.
|
||||
/// </summary>
|
||||
/// <returns>A new agent thread.</returns>
|
||||
public override AgentThread GetNewThread()
|
||||
public override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
|
||||
{
|
||||
AgentSessionId sessionId = this._context.NewAgentSessionId(this._agentName);
|
||||
return new DurableAgentThread(sessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an agent thread from JSON.
|
||||
/// </summary>
|
||||
/// <param name="serializedThread">The serialized thread data.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional JSON serializer options.</param>
|
||||
/// <returns>The deserialized agent thread.</returns>
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread DeserializeThread(
|
||||
JsonElement serializedThread,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
IAgentFeatureCollection? featureCollection = null)
|
||||
{
|
||||
return DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions);
|
||||
}
|
||||
@@ -63,7 +59,7 @@ public sealed class DurableAIAgent : AIAgent
|
||||
/// <exception cref="AgentNotRegisteredException">Thrown when the agent has not been registered.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the provided thread is not valid for a durable agent.</exception>
|
||||
/// <exception cref="NotSupportedException">Thrown when cancellation is requested (cancellation is not supported for durable agents).</exception>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -107,7 +103,7 @@ public sealed class DurableAIAgent : AIAgent
|
||||
{
|
||||
return await this._context.Entities.CallEntityAsync<AgentRunResponse>(
|
||||
durableThread.SessionId,
|
||||
nameof(AgentEntity.RunAgentAsync),
|
||||
nameof(AgentEntity.Run),
|
||||
request);
|
||||
}
|
||||
catch (EntityOperationFailedException e) when (e.FailureDetails.ErrorType == "EntityTaskNotFound")
|
||||
@@ -128,7 +124,7 @@ public sealed class DurableAIAgent : AIAgent
|
||||
/// <param name="options">Optional run options.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A streaming response enumerable.</returns>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -13,17 +13,18 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
|
||||
|
||||
public override AgentThread DeserializeThread(
|
||||
JsonElement serializedThread,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
IAgentFeatureCollection? featureCollection = null)
|
||||
{
|
||||
return DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions);
|
||||
}
|
||||
|
||||
public override AgentThread GetNewThread()
|
||||
public override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
|
||||
{
|
||||
return new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!));
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -70,7 +71,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
|
||||
return await agentRunHandle.ReadAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
|
||||
@@ -9,23 +9,67 @@ public sealed class DurableAgentsOptions
|
||||
{
|
||||
// Agent names are case-insensitive
|
||||
private readonly Dictionary<string, Func<IServiceProvider, AIAgent>> _agentFactories = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, TimeSpan?> _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal DurableAgentsOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default time-to-live (TTL) for agent entities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If an agent entity is idle for this duration, it will be automatically deleted.
|
||||
/// Defaults to 14 days. Set to <see langword="null"/> to disable TTL for agents without explicit TTL configuration.
|
||||
/// </remarks>
|
||||
public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum delay for scheduling TTL deletion signals. Defaults to 5 minutes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is primarily useful for testing (where shorter delays are needed) or for
|
||||
/// shorter-lived agents in workflows that need more rapid cleanup. The maximum allowed value is 5 minutes.
|
||||
/// Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions.
|
||||
/// However, this can also increase the load on the system and should be used with caution.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value exceeds 5 minutes.</exception>
|
||||
public TimeSpan MinimumTimeToLiveSignalDelay
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
const int MaximumDelayMinutes = 5;
|
||||
if (value > TimeSpan.FromMinutes(MaximumDelayMinutes))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
value,
|
||||
$"The minimum time-to-live signal delay cannot exceed {MaximumDelayMinutes} minutes.");
|
||||
}
|
||||
|
||||
field = value;
|
||||
}
|
||||
} = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an AI agent factory to the options.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="factory">The factory function to create the agent.</param>
|
||||
/// <param name="timeToLive">Optional time-to-live for this agent's entities. If not specified, uses <see cref="DefaultTimeToLive"/>.</param>
|
||||
/// <returns>The options instance.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="factory"/> is null.</exception>
|
||||
public DurableAgentsOptions AddAIAgentFactory(string name, Func<IServiceProvider, AIAgent> factory)
|
||||
public DurableAgentsOptions AddAIAgentFactory(string name, Func<IServiceProvider, AIAgent> factory, TimeSpan? timeToLive = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
this._agentFactories.Add(name, factory);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
this._agentTimeToLive[name] = timeToLive;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -50,12 +94,13 @@ public sealed class DurableAgentsOptions
|
||||
/// Adds an AI agent to the options.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent to add.</param>
|
||||
/// <param name="timeToLive">Optional time-to-live for this agent's entities. If not specified, uses <see cref="DefaultTimeToLive"/>.</param>
|
||||
/// <returns>The options instance.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="agent.Name"/> is null or whitespace or when an agent with the same name has already been registered.
|
||||
/// </exception>
|
||||
public DurableAgentsOptions AddAIAgent(AIAgent agent)
|
||||
public DurableAgentsOptions AddAIAgent(AIAgent agent, TimeSpan? timeToLive = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
@@ -70,6 +115,11 @@ public sealed class DurableAgentsOptions
|
||||
}
|
||||
|
||||
this._agentFactories.Add(agent.Name, sp => agent);
|
||||
if (timeToLive.HasValue)
|
||||
{
|
||||
this._agentTimeToLive[agent.Name] = timeToLive;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -81,4 +131,14 @@ public sealed class DurableAgentsOptions
|
||||
{
|
||||
return this._agentFactories.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time-to-live for a specific agent, or the default TTL if not specified.
|
||||
/// </summary>
|
||||
/// <param name="agentName">The name of the agent.</param>
|
||||
/// <returns>The time-to-live for the agent, or the default TTL if not specified.</returns>
|
||||
internal TimeSpan? GetTimeToLive(string agentName)
|
||||
{
|
||||
return this._agentTimeToLive.TryGetValue(agentName, out TimeSpan? ttl) ? ttl : this.DefaultTimeToLive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,15 @@ internal sealed class EntityAgentWrapper(
|
||||
private readonly IServiceProvider? _entityScopedServices = entityScopedServices;
|
||||
|
||||
// The ID of the agent is always the entity ID.
|
||||
public override string Id => this._entityContext.Id.ToString();
|
||||
protected override string? IdCore => this._entityContext.Id.ToString();
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
protected override async Task<AgentRunResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentRunResponse response = await base.RunAsync(
|
||||
AgentRunResponse response = await base.RunCoreAsync(
|
||||
messages,
|
||||
thread,
|
||||
this.GetAgentEntityRunOptions(options),
|
||||
@@ -37,13 +37,13 @@ internal sealed class EntityAgentWrapper(
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await foreach (AgentRunResponseUpdate update in base.RunStreamingAsync(
|
||||
await foreach (AgentRunResponseUpdate update in base.RunCoreStreamingAsync(
|
||||
messages,
|
||||
thread,
|
||||
this.GetAgentEntityRunOptions(options),
|
||||
|
||||
@@ -46,4 +46,58 @@ internal static partial class Logs
|
||||
Level = LogLevel.Information,
|
||||
Message = "Found response for agent with session ID '{SessionId}' with correlation ID '{CorrelationId}'")]
|
||||
public static partial void LogDonePollingForResponse(this ILogger logger, AgentSessionId sessionId, string correlationId);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 6,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL expiration time updated to {ExpirationTime:O}")]
|
||||
public static partial void LogTTLExpirationTimeUpdated(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime expirationTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 7,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL deletion signal scheduled for {ScheduledTime:O}")]
|
||||
public static partial void LogTTLDeletionScheduled(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime scheduledTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 8,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL deletion check running. Expiration time: {ExpirationTime:O}, Current time: {CurrentTime:O}")]
|
||||
public static partial void LogTTLDeletionCheck(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime? expirationTime,
|
||||
DateTime currentTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 9,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] Entity expired and deleted due to TTL. Expiration time: {ExpirationTime:O}")]
|
||||
public static partial void LogTTLEntityExpired(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime expirationTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 10,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL deletion signal rescheduled for {ScheduledTime:O}")]
|
||||
public static partial void LogTTLRescheduled(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId,
|
||||
DateTime scheduledTime);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 11,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{SessionId}] TTL expiration time cleared (TTL disabled)")]
|
||||
public static partial void LogTTLExpirationTimeCleared(
|
||||
this ILogger logger,
|
||||
AgentSessionId sessionId);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ public static class ServiceCollectionExtensions
|
||||
// The agent dictionary contains the real agent factories, which is used by the agent entities.
|
||||
services.AddSingleton(agents);
|
||||
|
||||
// Register the options so AgentEntity can access TTL configuration
|
||||
services.AddSingleton(options);
|
||||
|
||||
// The keyed services are used to resolve durable agent *proxy* instances for external clients.
|
||||
foreach (var factory in agents)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,13 @@ internal sealed class DurableAgentStateData
|
||||
[JsonPropertyName("conversationHistory")]
|
||||
public IList<DurableAgentStateEntry> ConversationHistory { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expiration time (UTC) for this agent entity.
|
||||
/// If the entity is idle beyond this time, it will be automatically deleted.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expirationTimeUtc")]
|
||||
public DateTime? ExpirationTimeUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any additional data found during deserialization that does not map to known properties.
|
||||
/// </summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
|
||||
}
|
||||
|
||||
HttpRequestData? httpRequestData = null;
|
||||
TaskEntityDispatcher? dispatcher = null;
|
||||
string? encodedEntityRequest = null;
|
||||
DurableTaskClient? durableTaskClient = null;
|
||||
ToolInvocationContext? mcpToolInvocationContext = null;
|
||||
|
||||
@@ -43,8 +43,8 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
|
||||
case HttpRequestData request:
|
||||
httpRequestData = request;
|
||||
break;
|
||||
case TaskEntityDispatcher entityDispatcher:
|
||||
dispatcher = entityDispatcher;
|
||||
case string entityRequest:
|
||||
encodedEntityRequest = entityRequest;
|
||||
break;
|
||||
case DurableTaskClient client:
|
||||
durableTaskClient = client;
|
||||
@@ -78,14 +78,14 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
|
||||
|
||||
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunAgentEntityFunctionEntryPoint)
|
||||
{
|
||||
if (dispatcher is null)
|
||||
if (encodedEntityRequest is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Task entity dispatcher binding is missing for the invocation {context.InvocationId}.");
|
||||
}
|
||||
|
||||
await BuiltInFunctions.InvokeAgentAsync(
|
||||
dispatcher,
|
||||
context.GetInvocationResult().Value = await BuiltInFunctions.InvokeAgentAsync(
|
||||
durableTaskClient,
|
||||
encodedEntityRequest,
|
||||
context);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.DurableTask.Worker.Grpc;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -22,14 +23,14 @@ internal static class BuiltInFunctions
|
||||
internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}";
|
||||
|
||||
// Exposed as an entity trigger via AgentFunctionsProvider
|
||||
public static async Task InvokeAgentAsync(
|
||||
[EntityTrigger] TaskEntityDispatcher dispatcher,
|
||||
public static Task<string> InvokeAgentAsync(
|
||||
[DurableClient] DurableTaskClient client,
|
||||
string encodedEntityRequest,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
// This should never be null except if the function trigger is misconfigured.
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
ArgumentNullException.ThrowIfNull(encodedEntityRequest);
|
||||
ArgumentNullException.ThrowIfNull(functionContext);
|
||||
|
||||
// Create a combined service provider that includes both the existing services
|
||||
@@ -38,7 +39,8 @@ internal static class BuiltInFunctions
|
||||
|
||||
// This method is the entry point for the agent entity.
|
||||
// It will be invoked by the Azure Functions runtime when the entity is called.
|
||||
await dispatcher.DispatchAsync(new AgentEntity(combinedServiceProvider, functionContext.CancellationToken));
|
||||
AgentEntity entity = new(combinedServiceProvider, functionContext.CancellationToken);
|
||||
return GrpcEntityRunner.LoadAndRunAsync(encodedEntityRequest, entity, combinedServiceProvider);
|
||||
}
|
||||
|
||||
public static async Task<HttpResponseData> RunAgentHttpAsync(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Release History
|
||||
|
||||
## <version>
|
||||
|
||||
- Addressed incompatibility issue with `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` >= 1.11.0 ([#2759](https://github.com/microsoft/agent-framework/pull/2759))
|
||||
|
||||
## v1.0.0-preview.251125.1
|
||||
|
||||
- Added support for .NET 10 ([#2128](https://github.com/microsoft/agent-framework/pull/2128))
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat
|
||||
Language = "dotnet-isolated",
|
||||
RawBindings =
|
||||
[
|
||||
"""{"name":"dispatcher","type":"entityTrigger","direction":"In"}""",
|
||||
"""{"name":"encodedEntityRequest","type":"entityTrigger","direction":"In"}""",
|
||||
"""{"name":"client","type":"durableClient","direction":"In"}"""
|
||||
],
|
||||
EntryPoint = BuiltInFunctions.RunAgentEntityFunctionEntryPoint,
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExt
|
||||
|
||||
path ??= $"/{agent.Name}/v1/chat/completions";
|
||||
var group = endpoints.MapGroup(path);
|
||||
var endpointAgentName = agent.DisplayName;
|
||||
var endpointAgentName = agent.Name ?? agent.Id;
|
||||
|
||||
group.MapPost("/", async ([FromBody] CreateChatCompletion request, CancellationToken cancellationToken)
|
||||
=> await AIAgentChatCompletionsProcessor.CreateChatCompletionAsync(agent, request, cancellationToken).ConfigureAwait(false))
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExt
|
||||
var handlers = new ResponsesHttpHandler(responsesService);
|
||||
|
||||
var group = endpoints.MapGroup(responsesPath);
|
||||
var endpointAgentName = agent.DisplayName;
|
||||
var endpointAgentName = agent.Name ?? agent.Id;
|
||||
|
||||
// Create response endpoint
|
||||
group.MapPost("/", handlers.CreateResponseAsync)
|
||||
|
||||
+11
-15
@@ -84,22 +84,18 @@ internal sealed class ConversationReferenceJsonConverter : JsonConverter<Convers
|
||||
return;
|
||||
}
|
||||
|
||||
// If only ID is present and no metadata, serialize as a simple string
|
||||
if (value.Metadata is null || value.Metadata.Count == 0)
|
||||
// Ideally if only ID is present and no metadata, we would serialize as a simple string.
|
||||
// However, while a request's "conversation" property can be either a string or an object
|
||||
// containing a string, a response's "conversation" property is always an object. Since
|
||||
// here we don't know which scenario we're in, we always serialize as an object, which works
|
||||
// in any scenario.
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("id", value.Id);
|
||||
if (value.Metadata is not null)
|
||||
{
|
||||
writer.WriteStringValue(value.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, serialize as an object
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("id", value.Id);
|
||||
if (value.Metadata is not null)
|
||||
{
|
||||
writer.WritePropertyName("metadata");
|
||||
JsonSerializer.Serialize(writer, value.Metadata, OpenAIHostingJsonContext.Default.DictionaryStringString);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
writer.WritePropertyName("metadata");
|
||||
JsonSerializer.Serialize(writer, value.Metadata, OpenAIHostingJsonContext.Default.DictionaryStringString);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,22 +73,22 @@ public static class AIAgentWithOpenAIExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the AI agent with a collection of OpenAI response items and returns the response as a native OpenAI <see cref="OpenAIResponse"/>.
|
||||
/// Runs the AI agent with a collection of OpenAI response items and returns the response as a native OpenAI <see cref="ResponseResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="agent">The AI agent to run.</param>
|
||||
/// <param name="messages">The collection of OpenAI response items to send to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="Task{OpenAIResponse}"/> representing the asynchronous operation that returns a native OpenAI <see cref="OpenAIResponse"/> response.</returns>
|
||||
/// <returns>A <see cref="Task{ResponseResult}"/> representing the asynchronous operation that returns a native OpenAI <see cref="ResponseResult"/> response.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> or <paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to an <see cref="OpenAIResponse"/>, typically when the underlying representation is not an OpenAI response.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to an <see cref="ResponseResult"/>, typically when the underlying representation is not an OpenAI response.</exception>
|
||||
/// <exception cref="NotSupportedException">Thrown when any message in <paramref name="messages"/> has a type that is not supported by the message conversion method.</exception>
|
||||
/// <remarks>
|
||||
/// This method converts the OpenAI response items to the Microsoft Extensions AI format using the appropriate conversion method,
|
||||
/// runs the agent with the converted message collection, and then extracts the native OpenAI <see cref="OpenAIResponse"/> from the response using <see cref="AgentRunResponseExtensions.AsOpenAIResponse"/>.
|
||||
/// runs the agent with the converted message collection, and then extracts the native OpenAI <see cref="ResponseResult"/> from the response using <see cref="AgentRunResponseExtensions.AsOpenAIResponse"/>.
|
||||
/// </remarks>
|
||||
public static async Task<OpenAIResponse> RunAsync(this AIAgent agent, IEnumerable<ResponseItem> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public static async Task<ResponseResult> RunAsync(this AIAgent agent, IEnumerable<ResponseItem> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(messages);
|
||||
|
||||
@@ -29,17 +29,17 @@ public static class AgentRunResponseExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or extracts a native OpenAI <see cref="OpenAIResponse"/> object from an <see cref="AgentRunResponse"/>.
|
||||
/// Creates or extracts a native OpenAI <see cref="ResponseResult"/> object from an <see cref="AgentRunResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The agent response.</param>
|
||||
/// <returns>The OpenAI <see cref="OpenAIResponse"/> object.</returns>
|
||||
/// <returns>The OpenAI <see cref="ResponseResult"/> object.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
|
||||
public static OpenAIResponse AsOpenAIResponse(this AgentRunResponse response)
|
||||
public static ResponseResult AsOpenAIResponse(this AgentRunResponse response)
|
||||
{
|
||||
Throw.IfNull(response);
|
||||
|
||||
return
|
||||
response.RawRepresentation as OpenAIResponse ??
|
||||
response.AsChatResponse().AsOpenAIResponse();
|
||||
response.RawRepresentation as ResponseResult ??
|
||||
response.AsChatResponse().AsOpenAIResponseResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace OpenAI.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="OpenAIResponseClient"/>
|
||||
/// Provides extension methods for <see cref="ResponsesClient"/>
|
||||
/// to simplify the creation of AI agents that work with OpenAI services.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -20,9 +20,9 @@ namespace OpenAI.Responses;
|
||||
public static class OpenAIResponseClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="OpenAIResponseClient"/> using the OpenAI Response API.
|
||||
/// Creates an AI agent from an <see cref="ResponsesClient"/> using the OpenAI Response API.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="OpenAIResponseClient" /> to use for the agent.</param>
|
||||
/// <param name="client">The <see cref="ResponsesClient" /> to use for the agent.</param>
|
||||
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
|
||||
/// <param name="name">Optional name for the agent for identification purposes.</param>
|
||||
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
|
||||
@@ -33,7 +33,7 @@ public static class OpenAIResponseClientExtensions
|
||||
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the OpenAI Response service.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static ChatClientAgent CreateAIAgent(
|
||||
this OpenAIResponseClient client,
|
||||
this ResponsesClient client,
|
||||
string? instructions = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
@@ -61,9 +61,9 @@ public static class OpenAIResponseClientExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="OpenAIResponseClient"/> using the OpenAI Response API.
|
||||
/// Creates an AI agent from an <see cref="ResponsesClient"/> using the OpenAI Response API.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="OpenAIResponseClient" /> to use for the agent.</param>
|
||||
/// <param name="client">The <see cref="ResponsesClient" /> to use for the agent.</param>
|
||||
/// <param name="options">Full set of options to configure the agent.</param>
|
||||
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
|
||||
@@ -71,7 +71,7 @@ public static class OpenAIResponseClientExtensions
|
||||
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the OpenAI Response service.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
|
||||
public static ChatClientAgent CreateAIAgent(
|
||||
this OpenAIResponseClient client,
|
||||
this ResponsesClient client,
|
||||
ChatClientAgentOptions options,
|
||||
Func<IChatClient, IChatClient>? clientFactory = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
|
||||
@@ -30,25 +30,25 @@ internal class PurviewAgent : AIAgent, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, IAgentFeatureCollection? featureCollection = null)
|
||||
{
|
||||
return this._innerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
|
||||
return this._innerAgent.DeserializeThread(serializedThread, jsonSerializerOptions, featureCollection);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread GetNewThread()
|
||||
public override AgentThread GetNewThread(IAgentFeatureCollection? featureCollection = null)
|
||||
{
|
||||
return this._innerAgent.GetNewThread();
|
||||
return this._innerAgent.GetNewThread(featureCollection);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._purviewWrapper.ProcessAgentContentAsync(messages, thread, options, this._innerAgent, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await this._purviewWrapper.ProcessAgentContentAsync(messages, thread, options, this._innerAgent, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var update in response.ToAgentRunResponseUpdates())
|
||||
|
||||
@@ -111,7 +111,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
if (inputArguments is not null)
|
||||
{
|
||||
JsonNode jsonNode = ConvertDictionaryToJson(inputArguments);
|
||||
ResponseCreationOptions responseCreationOptions = new();
|
||||
CreateResponseOptions responseCreationOptions = new();
|
||||
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
responseCreationOptions.Patch.Set("$.structured_inputs"u8, BinaryData.FromString(jsonNode.ToJsonString()));
|
||||
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
@@ -206,7 +206,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentResponseItem responseItem = await this.GetConversationClient().GetProjectConversationItemAsync(conversationId, messageId, include: null, cancellationToken).ConfigureAwait(false);
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
ResponseItem[] items = [responseItem.AsResponseResultItem()];
|
||||
return items.AsChatMessages().Single();
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
|
||||
await foreach (AgentResponseItem responseItem in this.GetConversationClient().GetProjectConversationItemsAsync(conversationId, null, limit, order.ToString(), after, before, include: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
ResponseItem[] items = [responseItem.AsResponseResultItem()];
|
||||
foreach (ChatMessage message in items.AsChatMessages())
|
||||
{
|
||||
yield return message;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user