Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba6b70d550 | |||
| 4d9c820995 | |||
| f4e9decca4 | |||
| eb220132ca | |||
| 8e3b86b1bc | |||
| 05241a2d5c |
@@ -2,7 +2,6 @@ dirs:
|
||||
- .
|
||||
excludedFiles:
|
||||
- ./python/CHANGELOG.md
|
||||
- "**/SKILL.md"
|
||||
ignorePatterns:
|
||||
- pattern: "/github/"
|
||||
- pattern: "./actions"
|
||||
@@ -27,7 +26,7 @@ ignorePatterns:
|
||||
- pattern: "https:\/\/dotnet.microsoft.com"
|
||||
- pattern: "https://github.com/Rel1cx/eslint-react"
|
||||
# excludedDirs:
|
||||
# Folders which include links to localhost, since it's not ignored with regular expressions
|
||||
# Folders which include links to localhost, since it's not ignored with regular expressions
|
||||
baseUrl: https://github.com/microsoft/agent-framework/
|
||||
aliveStatusCodes:
|
||||
- 200
|
||||
|
||||
+4
-127
@@ -1,130 +1,7 @@
|
||||
# Code ownership assignments
|
||||
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
#
|
||||
# Policy: a PR needs one approval, and it must come from a code owner of the changed
|
||||
# files ("Require review from Code Owners" + "Required approvals: 1" on `main`).
|
||||
# A PR touching several CODEOWNERS patterns will request review from the applicable
|
||||
# code owners, but an approval from any applicable code owner is sufficient to satisfy
|
||||
# GitHub's required-code-owner review.
|
||||
#
|
||||
# Order matters: the LAST matching pattern wins, so a module rule fully replaces the
|
||||
# catch-all rather than adding to it. @chetantoshniwal is included on every line as a
|
||||
# repository-wide fallback owner. All owners on a line have equal approval authority.
|
||||
#
|
||||
# CONVENTION: owners are written in the order
|
||||
# @chetantoshniwal <owner A> <owner B> [...]
|
||||
# @chetantoshniwal is at the beginning for aesthetics. The owners share equal approval
|
||||
# power and responsibility.
|
||||
#
|
||||
# RULE: every path must list at least two owners besides @chetantoshniwal. An author
|
||||
# cannot approve their own PR, so a path with a single module owner leaves only Chetan
|
||||
# to review whenever that owner is the author, which defeats the point of naming a
|
||||
# module owner.
|
||||
#
|
||||
# Samples: owned by all core developers of that language, not by the module a sample
|
||||
# demonstrates. Any core Python developer can approve any Python sample, and any core
|
||||
# .NET developer can approve any .NET sample. No dedicated rule is needed -- samples
|
||||
# fall through to the /python and /dotnet rules, which already list those developers.
|
||||
#
|
||||
# Tests: same as samples. Tests that live inside a package (python/packages/<pkg>/tests)
|
||||
# are covered by that package's rule instead, since they sit under its path.
|
||||
|
||||
# Default owners for everything not matched by a module rule below.
|
||||
* @chetantoshniwal @westey-m
|
||||
|
||||
# Repository-level paths: every core Agent Framework developer is a code owner, so any
|
||||
# one of them can approve. Be explicit now and we can use the AgentFramework team in the future.
|
||||
/docs/ @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
|
||||
/*.md @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
|
||||
/LICENSE @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
|
||||
/.gitattributes @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
|
||||
/.gitignore @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
|
||||
/.github @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
|
||||
|
||||
# Repository-level paths that require specific owners
|
||||
/.devcontainer @chetantoshniwal @westey-m @rogerbarreto @SergeyMenshykh
|
||||
/declarative-agents @chetantoshniwal @moonbox3 @peibekwe
|
||||
|
||||
# Core Python developers: @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
|
||||
/python @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
|
||||
|
||||
# Python packages
|
||||
/python/packages/a2a/ @chetantoshniwal @giles17 @eavanvalkenburg
|
||||
/python/packages/ag-ui/ @chetantoshniwal @moonbox3 @giles17
|
||||
/python/packages/anthropic/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/azure-ai-search/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/azure-contentunderstanding/ @chetantoshniwal @giles17 @eavanvalkenburg
|
||||
/python/packages/azure-cosmos/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/azure-cosmos-memory/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/bedrock/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/chatkit/ @chetantoshniwal @moonbox3 @giles17
|
||||
/python/packages/claude/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/copilotstudio/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/core/ @chetantoshniwal @eavanvalkenburg @moonbox3 @TaoChenOSU @giles17
|
||||
/python/packages/core/agent_framework/_workflows/ @chetantoshniwal @moonbox3 @TaoChenOSU
|
||||
/python/packages/core/agent_framework/_harness/ @chetantoshniwal @westey-m @eavanvalkenburg
|
||||
/python/packages/declarative/ @chetantoshniwal @moonbox3 @peibekwe
|
||||
/python/packages/devui/ @chetantoshniwal @eavanvalkenburg @moonbox3
|
||||
/python/packages/foundry/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3 @giles17
|
||||
/python/packages/foundry_hosting/ @chetantoshniwal @TaoChenOSU @eavanvalkenburg
|
||||
/python/packages/foundry_local/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/gemini/ @chetantoshniwal @giles17 @eavanvalkenburg
|
||||
/python/packages/github_copilot/ @chetantoshniwal @giles17 @eavanvalkenburg
|
||||
/python/packages/hosting/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
|
||||
/python/packages/hosting-a2a/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
|
||||
/python/packages/hosting-mcp/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
|
||||
/python/packages/hosting-responses/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
|
||||
/python/packages/hosting-telegram/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
|
||||
/python/packages/hyperlight/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/lab/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
|
||||
/python/packages/mem0/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/mistral/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/monty/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/ollama/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/openai/ @chetantoshniwal @eavanvalkenburg @moonbox3 @TaoChenOSU @giles17
|
||||
/python/packages/orchestrations/ @chetantoshniwal @moonbox3 @TaoChenOSU
|
||||
/python/packages/purview/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/redis/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
/python/packages/tools/ @chetantoshniwal @eavanvalkenburg @giles17
|
||||
|
||||
# Core .NET developers: @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
|
||||
# .NET projects
|
||||
/dotnet/src/Aspire.Hosting.AgentFramework.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/LegacySupport/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Shared/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Abstractions/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.AGUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Anthropic/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.CopilotStudio/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Declarative/ @chetantoshniwal @peibekwe @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Foundry/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/ @chetantoshniwal @rogerbarreto @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Harness/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.Hosting/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
|
||||
/dotnet/src/Microsoft.Agents.AI.Hyperlight/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.Mcp/ @chetantoshniwal @westey-m @peibekwe
|
||||
/dotnet/src/Microsoft.Agents.AI.Mem0/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.OpenAI/ @chetantoshniwal @westey-m @rogerbarreto
|
||||
/dotnet/src/Microsoft.Agents.AI.Purview/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.Valkey/ @chetantoshniwal @westey-m @SergeyMenshykh
|
||||
/dotnet/src/Microsoft.Agents.AI.Workflows/ @chetantoshniwal @peibekwe @rogerbarreto
|
||||
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ @chetantoshniwal @peibekwe @rogerbarreto
|
||||
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/ @chetantoshniwal @peibekwe @rogerbarreto
|
||||
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/ @chetantoshniwal @peibekwe @rogerbarreto
|
||||
/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/ @chetantoshniwal @peibekwe @rogerbarreto
|
||||
|
||||
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
|
||||
python/packages/durabletask/ @microsoft/agentframework-durabletask-developers
|
||||
python/samples/getting_started/azure_functions/ @microsoft/agentframework-durabletask-developers
|
||||
python/samples/getting_started/durabletask/ @microsoft/agentframework-durabletask-developers
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Azure Functions Integration Test Setup
|
||||
description: Prepare local emulators and tools for Azure Functions integration tests
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Start Durable Task Scheduler Emulator
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=dts-emulator)" ]; then
|
||||
echo "Stopping and removing existing Durable Task Scheduler Emulator"
|
||||
docker rm -f dts-emulator
|
||||
fi
|
||||
echo "Starting Durable Task Scheduler Emulator"
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 -e DTS_USE_DYNAMIC_TASK_HUBS=true mcr.microsoft.com/dts/dts-emulator:latest
|
||||
echo "Waiting for Durable Task Scheduler Emulator to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
|
||||
echo "Durable Task Scheduler Emulator is ready"
|
||||
- name: Start Azurite (Azure Storage emulator)
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=azurite)" ]; then
|
||||
echo "Stopping and removing existing Azurite (Azure Storage emulator)"
|
||||
docker rm -f azurite
|
||||
fi
|
||||
echo "Starting Azurite (Azure Storage emulator)"
|
||||
docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
|
||||
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: |
|
||||
echo "Installing Azure Functions Core Tools"
|
||||
npm install -g azure-functions-core-tools@4 --unsafe-perm true
|
||||
func --version
|
||||
@@ -17,7 +17,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Save Sample Playbooks
|
||||
description: >
|
||||
Save the cached sample-validation playbooks. Split out from
|
||||
sample-validation-setup (which only restores) so the save runs even when the
|
||||
validation step fails. Combining restore+save via actions/cache would skip the
|
||||
save on a failing job (post-if: success()), so freshly authored playbooks for
|
||||
samples that failed validation would never persist. Invoke this with
|
||||
'if: not-cancelled' after the validation step in each job.
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Save sample playbooks cache
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
# Must match the restore path/key in sample-validation-setup/action.yml and the
|
||||
# sample_validation --playbooks-dir default (samples/sample_validation/playbooks).
|
||||
path: python/samples/sample_validation/playbooks/
|
||||
key: sample-playbooks-${{ github.job }}-${{ github.run_id }}
|
||||
@@ -36,31 +36,15 @@ runs:
|
||||
shell: bash
|
||||
run: copilot --version && copilot -p "What can you do in one sentence?"
|
||||
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
os: ${{ inputs.os }}
|
||||
|
||||
- name: Restore sample playbooks
|
||||
# Restore-only. The matching save is a separate step in each job that runs with
|
||||
# `if: ${{ !cancelled() }}` (see .github/actions/sample-validation-save-playbooks).
|
||||
# A combined actions/cache would skip its post-job save on a failing job
|
||||
# (post-if: success()), so playbooks authored for samples that failed validation
|
||||
# would never persist. Keyed per job so each validate-* job keeps its own playbooks.
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
# Must match the sample_validation --playbooks-dir default, which resolves to
|
||||
# samples/sample_validation/playbooks (see python/scripts/sample_validation/__main__.py).
|
||||
# If a job overrides --playbooks-dir, update this path to match.
|
||||
path: python/samples/sample_validation/playbooks/
|
||||
key: sample-playbooks-${{ github.job }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
sample-playbooks-${{ github.job }}-
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ inputs.azure-client-id }}
|
||||
tenant-id: ${{ inputs.azure-tenant-id }}
|
||||
subscription-id: ${{ inputs.azure-subscription-id }}
|
||||
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
os: ${{ inputs.os }}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
applyTo: "dotnet/src/Microsoft.Agents.AI.DurableTask/**,dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**"
|
||||
---
|
||||
|
||||
# Durable Task area code instructions
|
||||
|
||||
The following guidelines apply to pull requests that modify files under
|
||||
`dotnet/src/Microsoft.Agents.AI.DurableTask/**` or
|
||||
`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**`:
|
||||
|
||||
## CHANGELOG.md
|
||||
|
||||
- Each pull request that modifies code should add just one bulleted entry to the `CHANGELOG.md` file containing a change title (usually the PR title) and a link to the PR itself.
|
||||
- New PRs should be added to the top of the `CHANGELOG.md` file under a "## [Unreleased]" heading.
|
||||
- If the PR is the first since the last release, the existing "## [Unreleased]" heading should be replaced with a "## v[X.Y.Z]" heading and the PRs since the last release should be added to the new "## [Unreleased]" heading.
|
||||
- The style of new `CHANGELOG.md` entries should match the style of the other entries in the file.
|
||||
- If the PR introduces a breaking change, the changelog entry should be prefixed with "[BREAKING]".
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -64,6 +64,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
github.event_name != 'issue_comment' ||
|
||||
(
|
||||
github.event.issue.pull_request &&
|
||||
github.event.comment.body == '/review' &&
|
||||
github.event.comment.body == '@devflow /review' &&
|
||||
(
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'OWNER'
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout GitHub automation
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
|
||||
sparse-checkout: |
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
steps:
|
||||
# Safe checkout: base repo only, not the untrusted PR head.
|
||||
- name: Checkout target repo base
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
|
||||
# Private DevFlow checkout: the PAT/token grants access to this repo's code.
|
||||
- name: Checkout DevFlow
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ env.DEVFLOW_REPOSITORY }}
|
||||
ref: ${{ env.DEVFLOW_REF }}
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
|
||||
@@ -38,9 +38,10 @@ jobs:
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
|
||||
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
|
||||
foundryHostingChanges: ${{ steps.filter.outputs.foundryHosting }}
|
||||
functionsChanged: ${{ steps.filter.outputs.functions }}
|
||||
coreChanged: ${{ steps.filter.outputs.core }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -65,6 +66,13 @@ jobs:
|
||||
- 'dotnet/Directory.Packages.props'
|
||||
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1'
|
||||
- '.github/workflows/dotnet-build-and-test.yml'
|
||||
functions:
|
||||
- 'dotnet/src/Microsoft.Agents.AI.DurableTask/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**'
|
||||
- 'dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/**'
|
||||
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/**'
|
||||
- '.github/actions/azure-functions-integration-setup/**'
|
||||
- '.github/workflows/dotnet-build-and-test.yml'
|
||||
core:
|
||||
- 'dotnet/src/Microsoft.Agents.AI/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
|
||||
@@ -103,7 +111,7 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -177,7 +185,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -234,6 +242,7 @@ jobs:
|
||||
-OutputPath dotnet/filtered-unit.slnx
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
|
||||
-TestProjectNameIncludeFilter "*IntegrationTests*" `
|
||||
-TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" `
|
||||
-OutputPath dotnet/filtered-integration.slnx
|
||||
|
||||
- name: Run Unit Tests
|
||||
@@ -355,7 +364,7 @@ jobs:
|
||||
env:
|
||||
configuration: Release
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -431,11 +440,113 @@ jobs:
|
||||
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
|
||||
# IT_HOSTED_AGENT_IMAGE was exported into $GITHUB_ENV by the previous step.
|
||||
|
||||
# DurableTask and AzureFunctions integration tests (ubuntu/net10.0 only).
|
||||
# Split from main dotnet-test job for path-based filtering and parallelism.
|
||||
dotnet-test-functions:
|
||||
needs: [paths-filter]
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
(needs.paths-filter.outputs.functionsChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true' ||
|
||||
github.event_name == 'schedule' ||
|
||||
github.event_name == 'workflow_dispatch')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Free runner disk space
|
||||
uses: ./.github/actions/free-runner-disk-space
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
- name: Build functions integration test projects
|
||||
shell: bash
|
||||
working-directory: dotnet
|
||||
run: |
|
||||
dotnet build ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests -c Release -f net10.0 --warnaserror
|
||||
dotnet build ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests -c Release -f net10.0 --warnaserror
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Set up Durable Task and Azure Functions Integration Test Emulators
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
|
||||
- name: Run Functions Integration Tests
|
||||
shell: pwsh
|
||||
working-directory: dotnet
|
||||
run: |
|
||||
# Run DurableTask integration tests
|
||||
dotnet test `
|
||||
--project ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests `
|
||||
-f net10.0 `
|
||||
-c Release `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--report-junit `
|
||||
--results-directory ../IntegrationTestResults/ `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-not-trait "Category=IntegrationDisabled" `
|
||||
--parallel-algorithm aggressive `
|
||||
--max-threads 2.0x
|
||||
|
||||
# Run AzureFunctions integration tests
|
||||
dotnet test `
|
||||
--project ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests `
|
||||
-f net10.0 `
|
||||
-c Release `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--report-junit `
|
||||
--results-directory ../IntegrationTestResults/ `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-not-trait "Category=IntegrationDisabled" `
|
||||
--parallel-algorithm aggressive `
|
||||
--max-threads 2.0x
|
||||
env:
|
||||
# OpenAI Models
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
# Azure OpenAI Models
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
|
||||
# Microsoft Foundry
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
|
||||
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
|
||||
|
||||
- name: Upload functions test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: dotnet-test-results-functions-net10.0-ubuntu-latest
|
||||
path: IntegrationTestResults/**/*.junit
|
||||
if-no-files-found: ignore
|
||||
|
||||
# This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
|
||||
dotnet-build-and-test-check:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dotnet-build, dotnet-test, dotnet-foundry-hosted-it]
|
||||
needs: [dotnet-build, dotnet-test, dotnet-foundry-hosted-it, dotnet-test-functions]
|
||||
steps:
|
||||
- name: Get Date
|
||||
shell: bash
|
||||
@@ -482,13 +593,13 @@ jobs:
|
||||
github.event_name != 'pull_request' &&
|
||||
(contains(join(needs.*.result, ','), 'success') ||
|
||||
contains(join(needs.*.result, ','), 'failure'))
|
||||
needs: [dotnet-test]
|
||||
needs: [dotnet-test, dotnet-test-functions]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -83,6 +83,10 @@ jobs:
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Set up Durable Task and Azure Functions Integration Test Emulators
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
|
||||
- name: Run Integration Tests
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -98,7 +102,7 @@ jobs:
|
||||
env:
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ github.token }}
|
||||
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
|
||||
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
environment: 'integration'
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
python-changes: ${{ steps.detect-changes.outputs.python }}
|
||||
steps:
|
||||
- name: Check out trusted workflow helpers
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/actions/github-app-token
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
steps:
|
||||
# Safe checkout: base repo only.
|
||||
- name: Checkout target repo base
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
# Private DevFlow (maf-dashboard) checkout.
|
||||
- name: Checkout DevFlow
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ env.DEVFLOW_REPOSITORY }}
|
||||
ref: ${{ env.DEVFLOW_REF }}
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout GitHub automation
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/actions/github-app-token
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
sparse-checkout: |
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
is_team_member: ${{ steps.check.outputs.is_team_member }}
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
sparse-checkout: |
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
sparse-checkout: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
# check out the latest version of the code
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ~/.cache/prek
|
||||
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
# installability starts differing across supported Python versions.
|
||||
UV_PYTHON: "3.13"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
working-directory: .
|
||||
- name: Cache Ollama models
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
@@ -232,12 +232,11 @@ jobs:
|
||||
fallback_url: ${{ env.LOCAL_MCP_URL }}
|
||||
- name: Prefer local MCP URL when available
|
||||
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
|
||||
- name: Test with pytest (Anthropic, Hyperlight, Mistral, Ollama, MCP integration)
|
||||
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/anthropic/tests
|
||||
packages/hyperlight/tests
|
||||
packages/mistral/tests
|
||||
packages/ollama/tests
|
||||
packages/core/tests/core/test_mcp.py
|
||||
packages/hosting-mcp/tests
|
||||
@@ -274,6 +273,73 @@ jobs:
|
||||
done
|
||||
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
|
||||
|
||||
# Azure Functions + Durable Task integration tests
|
||||
python-tests-functions:
|
||||
name: Python Integration Tests - Functions
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FUNCTIONS_WORKER_RUNTIME: "python"
|
||||
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
||||
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Set up Azure Functions Integration Test Emulators
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
- name: Test with pytest (Functions + Durable Task integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/azurefunctions/tests/integration_tests
|
||||
packages/durabletask/tests/integration_tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
-x
|
||||
--timeout=480 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-functions
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Foundry integration tests
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
@@ -297,7 +363,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -347,7 +413,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -402,7 +468,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -443,13 +509,13 @@ jobs:
|
||||
contents: read
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_COPILOT_TIMEOUT: "120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -487,6 +553,7 @@ jobs:
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
@@ -497,7 +564,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
@@ -551,6 +618,7 @@ jobs:
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
outputs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
|
||||
@@ -36,12 +36,13 @@ jobs:
|
||||
openaiChanged: ${{ steps.filter.outputs.openai }}
|
||||
azureChanged: ${{ steps.filter.outputs.azure }}
|
||||
miscChanged: ${{ steps.filter.outputs.misc }}
|
||||
functionsChanged: ${{ steps.filter.outputs.functions }}
|
||||
foundryChanged: ${{ steps.filter.outputs.foundry }}
|
||||
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
|
||||
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
|
||||
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -67,7 +68,6 @@ jobs:
|
||||
misc:
|
||||
- 'python/packages/anthropic/**'
|
||||
- 'python/packages/hyperlight/**'
|
||||
- 'python/packages/mistral/**'
|
||||
- 'python/packages/ollama/**'
|
||||
- 'python/packages/core/agent_framework/_mcp.py'
|
||||
- 'python/packages/core/tests/core/test_mcp.py'
|
||||
@@ -76,6 +76,9 @@ jobs:
|
||||
- '.github/actions/setup-local-mcp-server/**'
|
||||
- '.github/workflows/python-merge-tests.yml'
|
||||
- '.github/workflows/python-integration-tests.yml'
|
||||
functions:
|
||||
- 'python/packages/azurefunctions/**'
|
||||
- 'python/packages/durabletask/**'
|
||||
foundry:
|
||||
- 'python/packages/foundry/**'
|
||||
- 'python/samples/**/providers/foundry/**'
|
||||
@@ -107,7 +110,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -154,7 +157,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -215,7 +218,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -285,7 +288,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -296,7 +299,7 @@ jobs:
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
working-directory: .
|
||||
- name: Cache Ollama models
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
@@ -336,12 +339,11 @@ jobs:
|
||||
fallback_url: ${{ env.LOCAL_MCP_URL }}
|
||||
- name: Prefer local MCP URL when available
|
||||
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
|
||||
- name: Test with pytest (Anthropic, Hyperlight, Mistral, Ollama, MCP integration)
|
||||
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/anthropic/tests
|
||||
packages/hyperlight/tests
|
||||
packages/mistral/tests
|
||||
packages/ollama/tests
|
||||
packages/core/tests/core/test_mcp.py
|
||||
packages/hosting-mcp/tests
|
||||
@@ -388,6 +390,84 @@ jobs:
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Azure Functions + Durable Task integration tests
|
||||
python-tests-functions:
|
||||
name: Python Tests - Functions Integration
|
||||
needs: paths-filter
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
needs.paths-filter.outputs.pythonChanges == 'true' &&
|
||||
(github.event_name != 'merge_group' ||
|
||||
needs.paths-filter.outputs.functionsChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FUNCTIONS_WORKER_RUNTIME: "python"
|
||||
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
||||
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Set up Azure Functions Integration Test Emulators
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
- name: Test with pytest (Functions + Durable Task integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/azurefunctions/tests/integration_tests
|
||||
packages/durabletask/tests/integration_tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
-x
|
||||
--timeout=480 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Functions integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-functions
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
needs: paths-filter
|
||||
@@ -413,7 +493,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -474,7 +554,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -545,7 +625,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -600,13 +680,13 @@ jobs:
|
||||
contents: read
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_COPILOT_TIMEOUT: "120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -650,6 +730,7 @@ jobs:
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
@@ -660,7 +741,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
@@ -711,6 +792,7 @@ jobs:
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
|
||||
@@ -8,11 +8,8 @@ on:
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_COPILOT_MODEL: auto
|
||||
|
||||
permissions:
|
||||
copilot-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
@@ -23,13 +20,13 @@ jobs:
|
||||
environment: integration
|
||||
env:
|
||||
# Required configuration for get-started samples
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -48,10 +45,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -61,13 +54,12 @@ jobs:
|
||||
|
||||
validate-02-agents:
|
||||
name: Validate 02-agents
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Foundry configuration
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
@@ -75,19 +67,19 @@ jobs:
|
||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||
# OpenAI configuration
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
# GitHub MCP
|
||||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
# Observability
|
||||
ENABLE_INSTRUMENTATION: "true"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -113,11 +105,7 @@ jobs:
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers harness tools --save-report --report-name 02-agents
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers --save-report --report-name 02-agents
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
@@ -126,110 +114,20 @@ jobs:
|
||||
name: validation-report-02-agents
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-harness:
|
||||
name: Validate 02-agents/harness
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
# Optional: enables the Foundry memory path in harness samples
|
||||
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
|
||||
FOUNDRY_MEMORY_STORE: ${{ vars.FOUNDRY_MEMORY_STORE || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
||||
echo "FOUNDRY_EMBEDDING_MODEL=$FOUNDRY_EMBEDDING_MODEL" >> .env
|
||||
echo "FOUNDRY_MEMORY_STORE=$FOUNDRY_MEMORY_STORE" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/harness --save-report --report-name 02-agents-harness
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-harness
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-tools:
|
||||
name: Validate 02-agents/tools
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/tools --save-report --report-name 02-agents-tools
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-tools
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-openai:
|
||||
name: Validate 02-agents/providers/openai
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -250,10 +148,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -263,7 +157,6 @@ jobs:
|
||||
|
||||
validate-02-agents-azure:
|
||||
name: Validate 02-agents/providers/azure
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
@@ -274,7 +167,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -294,10 +187,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -307,7 +196,6 @@ jobs:
|
||||
|
||||
validate-02-agents-anthropic:
|
||||
name: Validate 02-agents/providers/anthropic
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
@@ -317,7 +205,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -336,10 +224,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -349,14 +233,20 @@ jobs:
|
||||
|
||||
validate-02-agents-github-copilot:
|
||||
name: Validate 02-agents/providers/github_copilot
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
permissions:
|
||||
copilot-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_COPILOT_MODEL: claude-opus-4.6
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -370,10 +260,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -392,7 +278,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -406,10 +292,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -428,7 +310,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -442,10 +324,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -455,19 +333,19 @@ jobs:
|
||||
|
||||
validate-02-agents-foundry:
|
||||
name: Validate 02-agents/providers/foundry
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME || '' }}
|
||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -488,10 +366,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -513,7 +387,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -534,10 +408,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -553,7 +423,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -567,10 +437,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -580,17 +446,16 @@ jobs:
|
||||
|
||||
validate-03-workflows:
|
||||
name: Validate 03-workflows
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -609,10 +474,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -620,68 +481,21 @@ jobs:
|
||||
name: validation-report-03-workflows
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-04-hosting-foundry-hosted-agents:
|
||||
name: Validate 04-hosting (foundry-hosted-agents)
|
||||
validate-04-hosting:
|
||||
name: Validate 04-hosting
|
||||
if: false # Temporarily disabled because of sample complexity
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
# Foundry hosted agent configuration
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ID: ${{ vars.FOUNDRY_PROJECT_ID }}
|
||||
AZURE_CONTAINER_REGISTRY_ENDPOINT: ${{ vars.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
|
||||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||||
TOOLBOX_ENDPOINT: ${{ vars.TOOLBOX_ENDPOINT }}
|
||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_NAME }}
|
||||
MEMORY_STORE_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_MEMORY_STORE }}
|
||||
AZURE_SEARCH_ENDPOINT: ${{ vars.AZURE_SEARCH_ENDPOINT }}
|
||||
AZURE_SEARCH_INDEX_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_SEARCH_INDEX_NAME }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Run sample validation
|
||||
# Maximum parallel workers is set to 1 because all samples use the same port
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 04-hosting/foundry-hosted-agents --save-report --report-name 04-hosting-foundry-hosted-agents --max-parallel-workers 1
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-04-hosting-foundry-hosted-agents
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-04-hosting-other:
|
||||
name: Validate 04-hosting (other)
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# A2A configuration
|
||||
A2A_AGENT_HOST: http://localhost:5001/
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -693,17 +507,13 @@ jobs:
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 04-hosting --exclude foundry-hosted-agents --save-report --report-name 04-hosting-other
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
cd scripts && uv run python -m sample_validation --subdir 04-hosting --save-report --report-name 04-hosting
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-04-hosting-other
|
||||
name: validation-report-04-hosting
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-05-end-to-end:
|
||||
@@ -712,8 +522,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
@@ -728,7 +538,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -742,10 +552,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -758,21 +564,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# OpenAI configuration
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -792,16 +598,9 @@ jobs:
|
||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
||||
|
||||
- name: Pre-install AutoGen dependencies for migration samples
|
||||
run: uv pip install "autogen-agentchat" "autogen-ext[openai]"
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration --agent-timeout 600
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
@@ -812,25 +611,23 @@ jobs:
|
||||
|
||||
validate-semantic-kernel-migration:
|
||||
name: Validate semantic-kernel-migration
|
||||
if: false # Temporarily disabled - to free up Copilot quota for other jobs
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration for AF
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration for SK
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
# OpenAI key
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
# OpenAI configuration for SK
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
# Copilot Studio
|
||||
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
||||
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
||||
@@ -840,7 +637,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
@@ -868,10 +665,6 @@ jobs:
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
|
||||
|
||||
- name: Save sample playbooks
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/sample-validation-save-playbooks
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
@@ -886,8 +679,6 @@ jobs:
|
||||
needs:
|
||||
- validate-01-get-started
|
||||
- validate-02-agents
|
||||
- validate-02-agents-harness
|
||||
- validate-02-agents-tools
|
||||
- validate-02-agents-openai
|
||||
- validate-02-agents-azure
|
||||
- validate-02-agents-anthropic
|
||||
@@ -898,13 +689,12 @@ jobs:
|
||||
- validate-02-agents-copilotstudio
|
||||
- validate-02-agents-custom
|
||||
- validate-03-workflows
|
||||
- validate-04-hosting-foundry-hosted-agents
|
||||
- validate-04-hosting-other
|
||||
- validate-04-hosting
|
||||
- validate-05-end-to-end
|
||||
- validate-autogen-migration
|
||||
- validate-semantic-kernel-migration
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Download all validation reports
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Download coverage report
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Get GitHub automation token
|
||||
id: github-auth
|
||||
|
||||
@@ -161,19 +161,19 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
||||
|
||||
### Python
|
||||
|
||||
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to workflows
|
||||
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
|
||||
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
|
||||
- [Workflows](./python/samples/03-workflows): workflow creation and integration with agents
|
||||
- [Hosting](./python/samples/04-hosting): A2A, self-hosted protocol helpers, and Foundry hosted agents. Durable Task and Azure Functions samples are in the [Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples).
|
||||
- [Hosting](./python/samples/04-hosting): A2A, Azure Functions, Durable Task hosting
|
||||
- [End-to-End](./python/samples/05-end-to-end): full applications, evaluation, and demos
|
||||
|
||||
### .NET
|
||||
|
||||
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to workflows
|
||||
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
|
||||
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
|
||||
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
|
||||
- [Workflows](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
|
||||
- [Hosting](./dotnet/samples/04-hosting): A2A and Foundry hosted agents. Durable agent and workflow samples are in the [Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples).
|
||||
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
|
||||
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
|
||||
|
||||
## Community & Feedback
|
||||
@@ -199,7 +199,6 @@ For environment variable configuration specific to each sample, refer to the REA
|
||||
## Contributor Resources
|
||||
|
||||
- [Contributing Guide](./CONTRIBUTING.md)
|
||||
- [Code of Conduct](./CODE_OF_CONDUCT.md)
|
||||
- [Python Development Guide](./python/DEV_SETUP.md)
|
||||
- [Design Documents](./docs/design)
|
||||
- [Architectural Decision Records](./docs/decisions)
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
status: Accepted
|
||||
contact: cgillum
|
||||
date: 2026-07-21
|
||||
deciders: cgillum, vrdmr, chetantoshniwal
|
||||
consulted: westey-m, eavanvalkenburg, kshyju, larohra, ahmedmuhsin
|
||||
informed:
|
||||
---
|
||||
|
||||
# Extract Durable Task and Azure Functions hosting into a separate repository
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The Durable Task and Azure Functions hosting integrations (`agent-framework-durabletask`,
|
||||
`agent-framework-azurefunctions`, plus their samples, docs, and CI) currently live in the
|
||||
`microsoft/agent-framework` (MAF) monorepo. They carry heavyweight specialized dependencies
|
||||
(Azure Functions runtime, Durable Task) and need integration-test infrastructure (Functions Core
|
||||
Tools, Azurite, a DTS emulator) that the core repo otherwise does not.
|
||||
|
||||
This ADR proposes moving them into a dedicated repository
|
||||
([`microsoft/agent-framework-durable-extension`](https://github.com/microsoft/agent-framework-durable-extension))
|
||||
and considers how to do so without breaking existing users who import them today.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Independent lifecycle** — the hosting integrations should be able to version and release on their
|
||||
own cadence, decoupled from core (extends [ADR-0008](0008-python-subpackages.md)'s goal of keeping
|
||||
heavyweight/optional dependencies out of the main package).
|
||||
- **Dependency & CI isolation** — keep core lean and its PR pipeline free of heavyweight hosting
|
||||
dependencies and integration-test prerequisites.
|
||||
- **Ownership** — a dedicated repo would give the integrations their own issues, CODEOWNERS, and
|
||||
contribution flow.
|
||||
- **No breaking change** — existing `from agent_framework.azure import …` code and
|
||||
`pip install agent-framework[all]` should keep working (stable-import-path guarantee, ADR-0008).
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep in the MAF repo** (status quo).
|
||||
2. **Move out, drop the core shim** — the extension becomes standalone; core stops re-exporting the
|
||||
types and removes them from `[all]`.
|
||||
3. **Move out, keep core's backward-compat shim + `[all]`** (proposed) — the code would live in the
|
||||
new repo; core would still lazily re-export the entry-point types from `agent_framework.azure` and
|
||||
keep both packages in the `[all]` extra (resolved from PyPI).
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Proposed choice: **Option 3.** Extract the integrations for lifecycle, dependency, and ownership
|
||||
isolation, while preserving the existing import surface so the move is invisible to consumers.
|
||||
Option 1 forgoes the isolation benefits; Option 2 achieves them but would be a breaking change for
|
||||
existing imports and the `[all]` extra.
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good — would give independent release cadence, a leaner/faster core repo and CI, and clear
|
||||
ownership for the hosting integrations.
|
||||
- Good — no user-visible break: existing imports and `agent-framework[all]` would continue to work
|
||||
unchanged.
|
||||
- Neutral — type *definitions* would live once in the extension; the core shim would re-export only a
|
||||
curated subset of entry-point types (no metadata duplication). The extension's own samples/docs
|
||||
would import directly from `agent_framework_durabletask` / `agent_framework_azurefunctions`; the
|
||||
shim would be compatibility-only.
|
||||
- Neutral — users may still open GitHub issues against the core repo for problems in the extension,
|
||||
but the extension's own repo would be the primary place for issues and PRs. These issues would
|
||||
need to be triaged and transferred to the extension repo.
|
||||
- Neutral — **.NET public API boundary.** The extension should prefer the smallest stable public core
|
||||
API over friend-assembly access where the capability is useful to external hosts or tooling. For
|
||||
workflow routing metadata, the agreed first step is to expose a read-only `Workflow.Edges` view plus
|
||||
public `EdgeData.Connection` and `FanOutEdgeData`, while keeping graph construction internal
|
||||
([#7448](https://github.com/microsoft/agent-framework/issues/7448),
|
||||
[#7459](https://github.com/microsoft/agent-framework/pull/7459)). This reduces internal coupling but
|
||||
adds a public API compatibility commitment. Any remaining internal dependencies would still need to
|
||||
be evaluated individually before retaining `InternalsVisibleTo`.
|
||||
- Bad — **Python version coordination.** Core's shim correctness would track the extension's publish
|
||||
cadence. In the other direction, when an extension package adopts a new core API, maintainers would
|
||||
need to choose per feature between raising its minimum core version (simpler, but forces every
|
||||
extension user to upgrade) and conditional imports with fallback behavior (preserves support for
|
||||
older core versions, but adds implementation and testing complexity).
|
||||
|
||||
## Validation
|
||||
|
||||
Compliance would be validated by:
|
||||
|
||||
- Python: `uv lock --check` passing with both packages resolving from PyPI; the shim entry-point
|
||||
symbols importing at runtime after `uv sync --all-extras`; `pyright` staying clean on
|
||||
`agent_framework/azure/__init__.pyi`; and extension tests running against both the minimum supported
|
||||
and current core versions when conditional compatibility behavior is used.
|
||||
- .NET: tests from an external assembly confirming that workflow routing metadata is inspectable
|
||||
through the agreed public surface while graph construction remains internal.
|
||||
|
||||
A known risk is **publish-lag**: if a symbol is added to core's shim before the extension has
|
||||
published a release that exports it, that symbol would not resolve at runtime. The mitigation would
|
||||
be to omit any such symbol from the shim until the extension publishes it, then add the entry and
|
||||
re-lock.
|
||||
|
||||
## More Information
|
||||
|
||||
- Related: [ADR-0008](0008-python-subpackages.md) (vendor namespaces + stable import paths),
|
||||
[ADR-0021](0021-provider-leading-clients.md) (lazy-loading gateways),
|
||||
[issue #7448](https://github.com/microsoft/agent-framework/issues/7448) and
|
||||
[PR #7459](https://github.com/microsoft/agent-framework/pull/7459) (.NET workflow routing API).
|
||||
- Follow-ups: during extraction, keep the shim's re-exported symbols in sync with each newly
|
||||
published extension release (adding any symbol only once the extension publishes it); document the
|
||||
direct-import convention in the extension's samples READMEs so samples are not switched back to the
|
||||
shim.
|
||||
@@ -1,308 +0,0 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-07-24
|
||||
deciders: eavanvalkenburg, chetantoshnival, taochenosu, moonbox3, giles17
|
||||
---
|
||||
|
||||
# Python session storage and serialization
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Python does not have a broadly shared session-store API in
|
||||
`agent-framework-core`. The alpha `agent-framework-hosting` package has a small process-local `SessionStore`, but that
|
||||
type is hosting-specific, in-memory only, and unavailable to packages such as Foundry Hosting without taking a
|
||||
dependency on the hosting helper package.
|
||||
|
||||
The alpha implementation is a prototype, not a compatibility constraint. This decision may replace its location,
|
||||
names, method shape, and behavior if another design is preferable.
|
||||
|
||||
The existing file-backed persistence surfaces solve narrower problems:
|
||||
|
||||
- `FileHistoryProvider` stores conversation `Message` records, not complete `AgentSession` snapshots;
|
||||
- `FileCheckpointStorage` stores workflow checkpoints; and
|
||||
- the Responses provider stores protocol history, but not Agent Framework runtime state carried in
|
||||
`AgentSession.state`.
|
||||
|
||||
`AgentSession.to_dict()` / `from_dict()` already provide a dictionary snapshot shape. Session state may contain
|
||||
framework or application-defined objects, and `register_state_type` provides dynamic type restoration, but the
|
||||
registration and collision behavior is not yet strong enough to serve as a durable, cold-start persistence contract.
|
||||
|
||||
The framework therefore needs to decide:
|
||||
|
||||
- where a reusable in-memory and file-backed session store belongs;
|
||||
- how a complete `AgentSession` should be serialized atomically and validated;
|
||||
- how custom nested state types are registered and restored after process restart; and
|
||||
- how to provide the required readable JSON format while leaving room for an optional optimized binary format.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
### Session-store ownership and API
|
||||
|
||||
- Make session storage reusable by core, hosting, and provider packages without creating dependency cycles.
|
||||
- Keep the smallest public API that supports in-memory use, durable implementations, and application-defined stores.
|
||||
- Define the minimum async operations required for lookup, replacement, and deletion.
|
||||
- Decide explicitly whether reads return shared instances or independent snapshots suitable for branching.
|
||||
- Simpler is better
|
||||
|
||||
### Serialization and type restoration
|
||||
|
||||
- Provide readable JSON serialization as a required capability.
|
||||
- Treat an optimized binary format as a nice-to-have only when the chosen JSON implementation supports it without a
|
||||
separate state model or substantial additional complexity.
|
||||
- Perform one typed encode and decode operation per file write/read.
|
||||
- Preserve dynamic registration of nested state types by the provider modules that own them.
|
||||
- Fail before persistence when an object cannot be restored after a cold start.
|
||||
- Keep the existing serialized `{"type": "<id>", ...}` representation compatible.
|
||||
|
||||
## Decision 1: Session-store ownership and API shape
|
||||
|
||||
### Keep `SessionStore` in `agent-framework-hosting`
|
||||
|
||||
- Good: keeps the abstraction local to app-owned hosting scenarios.
|
||||
- Bad: Foundry Hosting and other packages cannot reuse it without depending on the hosting helper package.
|
||||
- Bad: a generic session snapshot store is not inherently or only a web-hosting concern.
|
||||
- Bad: durable implementations would either be duplicated or placed in an unrelated package.
|
||||
|
||||
### Add an abstract store plus separate in-memory and file implementations
|
||||
|
||||
For example, define a `SessionStore` protocol/ABC with `InMemorySessionStore` and `FileSessionStore`.
|
||||
|
||||
- Good: clearly separates the contract from implementations.
|
||||
- Good: implementation names state their storage behavior explicitly.
|
||||
- Neutral: follows a familiar repository/adapter pattern.
|
||||
- Bad: introduces an additional public type and rename for a three-method experimental API.
|
||||
- Bad: callers must choose an implementation even for the default in-memory case.
|
||||
- Bad: the abstraction adds little value while every implementation still needs the same method overrides.
|
||||
|
||||
### Move the concrete store to core and use it as the overridable base
|
||||
|
||||
Move `SessionStore` to `agent-framework-core`, retain its in-memory behavior, and implement `FileSessionStore` by
|
||||
overriding the same async methods.
|
||||
|
||||
- Good: one public type is both the useful default and the extension point.
|
||||
- Good: existing custom stores can continue subclassing and overriding `get` / `set` / `delete`.
|
||||
- Good: core and provider packages can share the API without depending on hosting helpers.
|
||||
- Good: `FileSessionStore` remains a focused subclass while the base stays free of file-system concerns.
|
||||
- Bad: the class name does not explicitly say "in memory" when used without overrides.
|
||||
|
||||
## Decision 2: Serialization and type restoration
|
||||
|
||||
Once a file-backed store exists, it needs an on-disk format and a reliable way to reconstruct the complete
|
||||
`AgentSession`, including nested framework and application-defined state. Serialization belongs to each durable store
|
||||
implementation rather than the `SessionStore` API: the default in-memory store does not serialize, and custom stores
|
||||
remain free to choose another protocol.
|
||||
|
||||
The alternatives below compare top-level snapshot validation, JSON encoding/decoding cost, and how each option
|
||||
interacts with the dynamic custom-state registry. Binary storage is not a primary selection criterion.
|
||||
|
||||
### Considered options
|
||||
|
||||
The standard-library and optimized-JSON options are not mutually exclusive. A store can default to `json` while
|
||||
accepting caller-supplied `dumps` / `loads` callables for `orjson` or another compatible implementation. This is the
|
||||
pre-msgspec `FileHistoryProvider` design; those hooks remain only as a deprecated compatibility path.
|
||||
|
||||
### Standard library `json`
|
||||
|
||||
- Good: no additional dependency and familiar readable output.
|
||||
- Good: accepts the existing dictionary snapshots without a schema.
|
||||
- Good: can remain the fallback/default behind pluggable `dumps` / `loads`.
|
||||
- Neutral: custom state restoration still requires the framework registry.
|
||||
- Bad: slower encoding and decoding than optimized native implementations.
|
||||
- Bad: provides no typed snapshot validation during file reads.
|
||||
|
||||
### Optimized drop-in JSON libraries such as `orjson`
|
||||
|
||||
- Good: substantially faster JSON encoding and decoding than the standard library.
|
||||
- Good: can preserve the existing dictionary-oriented snapshot and custom `dumps` / `loads` shape.
|
||||
- Good: can be an opt-in codec without making the optimized package a framework dependency.
|
||||
- Neutral: returns bytes when encoding, which the file stores can already handle.
|
||||
- Neutral: custom state restoration still requires the framework registry.
|
||||
- Bad: remains an untyped top-level decode; the framework must separately validate the session snapshot shape.
|
||||
- Bad: choosing one drop-in implementation as a core dependency adds a dependency without providing typed construction.
|
||||
|
||||
### Pydantic `model_dump` / `model_validate`
|
||||
|
||||
- Good: Pydantic is already a core dependency.
|
||||
- Good: a typed session snapshot model can validate top-level fields and provide `model_dump_json` /
|
||||
`model_validate_json` for file serialization.
|
||||
- Good: validation errors include useful field paths.
|
||||
- Neutral: the dynamic `state` field remains `dict[str, Any]`, so custom nested state restoration still requires the
|
||||
framework registry.
|
||||
- Neutral: the public `AgentSession` does not need to become a Pydantic model; an internal snapshot model can bridge it.
|
||||
- Bad: benchmarked encode/decode includes model construction and dumping overhead on every operation.
|
||||
- Bad: core dependency on Pydantic run the risk of us not being able to use different versions or users of the framework being unable to upgrade or having additional extra code dealing with major version bumps in Pydantic.
|
||||
|
||||
### msgspec typed/tagged unions only
|
||||
|
||||
- Good: msgspec owns validation and reconstruction end to end.
|
||||
- Neutral: works well for a closed set of framework-owned `msgspec.Struct` types.
|
||||
- Bad: every external type must be known when the decoder schema is constructed; dynamic registration is lost.
|
||||
|
||||
### msgspec codecs plus an explicit dynamic registry
|
||||
|
||||
- Good: one typed file encode/decode and dynamic nested custom types.
|
||||
- Good: it satisfies the required readable JSON format.
|
||||
- Neutral: the same typed snapshot can also support optional MessagePack as a low-cost implementation detail.
|
||||
- Good: the registry can enforce stable IDs, codec completeness, and collision handling.
|
||||
- Neutral: a single state-payload hook still recursively applies registry codecs.
|
||||
- Bad: msgspec cannot infer dynamic types from JSON without the framework's type tags.
|
||||
|
||||
## Benchmark Evidence
|
||||
|
||||
A benchmark using a large `AgentSession` with 2,000 `Message` objects stored through
|
||||
`InMemoryHistoryProvider`, nested standard dictionaries, registered custom classes, and registered Pydantic models
|
||||
measured the complete `AgentSession.to_dict()` / codec / `AgentSession.from_dict()` path.
|
||||
The reproducible harness is
|
||||
[`python/scripts/session_serialization_benchmark.py`](../../python/scripts/session_serialization_benchmark.py):
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run --with orjson python scripts/session_serialization_benchmark.py
|
||||
```
|
||||
|
||||
| Codec | File size | Encode median (ms) | Decode median (ms) | Round-trip median (ms) | Disk round-trip median (ms) |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| Standard library JSON | 1.57 MiB | 33.503 | 14.316 | 55.261 | 75.226 |
|
||||
| orjson | 1.57 MiB | 25.808 | 11.754 | 39.398 | 63.319 |
|
||||
| Pydantic JSON | 1.57 MiB | 28.330 | 18.344 | 53.522 | 77.096 |
|
||||
| msgspec JSON | 1.57 MiB | 26.019 | 11.379 | **38.060** | 62.230 |
|
||||
| msgspec MessagePack | **1.45 MiB** | **25.134** | **11.201** | 38.512 | **58.112** |
|
||||
|
||||
The JSON encodings produced the same 1.57 MiB file size. msgspec JSON had the best median JSON round-trip latency,
|
||||
slightly ahead of orjson, while also supporting typed top-level decoding. Pydantic validation added measurable decode
|
||||
and disk-round-trip overhead without eliminating the dynamic state registry.
|
||||
|
||||
MessagePack reduced file size to 92.2% of JSON (about 7.8% smaller) and produced the best encode, decode, and disk
|
||||
round-trip medians. Its in-memory round-trip median was effectively tied with msgspec JSON. This supports offering it
|
||||
as a nice-to-have, but it is not required to justify choosing msgspec for JSON.
|
||||
|
||||
These results are workload- and machine-dependent. The small differences between optimized JSON implementations are
|
||||
not the basis for the architectural choice. The benchmark instead confirms that the typed design does not impose a
|
||||
material regression for this representative payload:
|
||||
|
||||
- use msgspec JSON as the readable default;
|
||||
- optionally offer msgspec MessagePack when storage size or disk latency matters;
|
||||
- retain the explicit registry for dynamic custom state in both formats;
|
||||
- do not add orjson solely for a small JSON performance difference without typed decoding; and
|
||||
- do not use Pydantic as the file codec when its validation overhead does not replace the registry.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### Decision 1: Move the concrete overridable store to core
|
||||
|
||||
`SessionStore` moves to `agent-framework-core` as an experimental public API. It remains a concrete in-memory store and
|
||||
the default used by `AgentState` in the `hosting` package. Its async `get`, `set`, and `delete` methods remain overridable for custom storage
|
||||
implementations.
|
||||
|
||||
`FileSessionStore` subclasses `SessionStore` and provides durable atomic file persistence. No separate
|
||||
`InMemorySessionStore`, protocol, or ABC is introduced. `agent-framework-hosting` consumes the core type and no longer
|
||||
owns or re-exports `SessionStore` (this will be a breaking change in the `hosting` package).
|
||||
|
||||
Actual `SessionStore` and `FileSessionStore` operations mark Python feature-usage index 17,
|
||||
`core.session_store`, following ADR-0033's use-not-presence policy. Construction and import alone do not mark the bit.
|
||||
|
||||
`SessionStore` accepts opaque non-empty keys so custom backends can use their native key contracts. `FileSessionStore`
|
||||
accepts opaque keys up to 128 characters and encodes values that are not portable filename stems; this supports
|
||||
provider IDs such as `telegram:<bot-id>:<chat-id>` without permitting path traversal. `AgentState` remains
|
||||
storage-agnostic and passes keys through unchanged; each store implementation owns backend-specific validation or
|
||||
normalization. Protocol-specific hosts such as Foundry may still derive their own stable storage key before calling the
|
||||
store.
|
||||
|
||||
Foundry Hosting exposes an experimental `FoundrySessionStore`, which is the
|
||||
default `ResponsesHostServer` store when hosted; local hosting defaults to the
|
||||
in-memory `SessionStore`. `FoundrySessionStore` currently subclasses
|
||||
`FileSessionStore`, stores snapshots under
|
||||
`/.sessions/<user-id>/<conversation-id-or-response-id>.json`, and derives the
|
||||
validated user partition from
|
||||
`azure.ai.agentserver.core.get_request_context()`. A Foundry session controls
|
||||
hosted compute and filesystem lifetime and may host multiple users and
|
||||
Responses conversations, so its ID is not used as the MAF session identifier.
|
||||
Stored-conversation requests read and write one snapshot under
|
||||
`conversation_id`. Response-chain requests read under `previous_response_id`
|
||||
and write the updated, loaded MAF session under the current `response_id`, which
|
||||
allows branching without overwriting the parent snapshot. Because Foundry does
|
||||
not infer `agent_session_id` from `previous_response_id`, response-chain callers
|
||||
must also reuse the prior response's hosted session ID so the request reaches
|
||||
the same persistent `$HOME`; conversation objects bind a stable hosted session
|
||||
automatically.
|
||||
The Foundry-specific type is the host configuration seam; its implementation
|
||||
may later move from files to a Foundry storage API without changing the generic
|
||||
core store contract. The session file API maps `/` to the hosted `$HOME`
|
||||
directory, so this API path is persisted on disk under `$HOME/.sessions`.
|
||||
|
||||
### Decision 2: Use msgspec codecs plus an explicit dynamic registry
|
||||
|
||||
Chosen option: **msgspec codecs plus an explicit dynamic registry**.
|
||||
|
||||
`FileSessionStore` uses a typed internal `msgspec.Struct` snapshot with reusable JSON and MessagePack encoders/decoders.
|
||||
JSON is the required and default format. Because msgspec can reuse the same typed snapshot and registry hooks,
|
||||
`serialization_format="msgpack"` is also exposed as an optional compact binary convenience. The complete state
|
||||
dictionary is wrapped in one custom field; its encode/decode hooks recursively translate explicitly registered types
|
||||
to and from the existing tagged mappings in either format.
|
||||
|
||||
The dependency range is `msgspec>=0.20.0,<0.22`: version 0.20.0 added Python 3.14 support, and the upper bound limits
|
||||
core to the tested 0.20/0.21 minor lines.
|
||||
|
||||
Three dependency placements were considered:
|
||||
|
||||
1. Make msgspec a standard core dependency.
|
||||
2. Make msgspec optional in core but standard in Foundry hosting.
|
||||
3. Make msgspec optional in both packages.
|
||||
|
||||
Option 3 moves installation failures to application developers even though durable session persistence is required for
|
||||
the primary `ResponsesHostServer` API to preserve Agent Framework state. Option 2 removes that burden from Foundry
|
||||
hosting but makes core's shared `_sessions` module and public types conditionally defined or lazily imported without
|
||||
removing msgspec from the default Foundry installation. Option 1 is therefore selected: msgspec is a standard core
|
||||
dependency, giving both core file providers and Foundry hosting one predictable implementation path.
|
||||
|
||||
Core already depends on the native `pydantic-core` extension, so native-wheel availability is not a new packaging
|
||||
constraint. The msgspec project is also actively tracking upcoming Python support; its merged
|
||||
[`Add 3.15-dev to CI` PR](https://github.com/msgspec/msgspec/pull/1037) exercises Python 3.15 development builds. This gives confidence that they will add support for new python version quickly.
|
||||
|
||||
The public `AgentSession` remains a normal framework class. The msgspec Struct is an internal persistence DTO rather
|
||||
than the inheritance base for runtime sessions. The Struct gives persistence one typed encode/decode operation, validates
|
||||
the snapshot envelope, and carries an explicit payload version. The benchmark's small timing spread was not used to
|
||||
choose the Struct.
|
||||
|
||||
`register_state_type` supports stable type IDs and optional codecs, rejects collisions, and provides defaults for
|
||||
`to_dict` / `from_dict` classes and Pydantic models. Type IDs share one process-wide registry, so provider packages
|
||||
should use stable package-qualified identifiers and register their own state types at module import time; consumers do
|
||||
not need to know those implementation details. One recursive serializer is shared by `AgentSession.to_dict()` and the
|
||||
durable codecs. The established implicit Pydantic registration behavior remains temporarily for compatibility, but now
|
||||
emits `DeprecationWarning`. Same-process round-trips continue to work; cold-start deserialization is not guaranteed
|
||||
without explicit provider registration. Unknown persisted type IDs remain raw dictionaries.
|
||||
|
||||
File snapshots are quarantined only when their bytes cannot be parsed as the selected JSON or MessagePack format.
|
||||
Schema errors, unsupported snapshot versions, and registered state-decoder failures leave the original file in place so
|
||||
an application fix, rollback, or compatible reader can recover it.
|
||||
|
||||
`FileHistoryProvider` also adds msgspec JSON as its default JSON Lines codec. It supports the same explicit
|
||||
`serialization_format="msgpack"` choice using length-prefixed append-only MessagePack records. Its existing `dumps` /
|
||||
`loads` extension points remain temporarily for JSON compatibility, emit `DeprecationWarning` when supplied, and do
|
||||
not apply to MessagePack. New code uses the built-in codecs. The default JSON reader falls back to the standard library
|
||||
for legacy JSON Lines containing `NaN` or infinity, and writes those non-finite values with the standard library so
|
||||
existing history semantics are preserved.
|
||||
|
||||
## Follow-up Work
|
||||
|
||||
Audit the remaining file-backed stores to determine whether they benefit from the same typed msgspec treatment and
|
||||
optional JSON / MessagePack formats. `FileCheckpointStorage` is the first candidate because it persists large,
|
||||
structured workflow state and currently uses JSON plus custom checkpoint value encoding. Its existing
|
||||
`WorkflowCheckpoint.version` field already provides a payload-shape discriminator.
|
||||
|
||||
Checkpoint migration should be reader-first. A compatibility release can detect the codec from the first byte, widen
|
||||
the two `glob("*.json")` readers to discover future formats, and continue writing only JSON. A later release can add
|
||||
opt-in MessagePack writes while retaining JSON as the default. The payload `version` should describe the checkpoint
|
||||
shape rather than the codec, which is discoverable from the bytes. MessagePack should not become the default while
|
||||
mixed-version fleets may share one checkpoint directory: older readers silently ignore non-JSON files and could resume
|
||||
from no checkpoint instead of surfacing an incompatibility.
|
||||
|
||||
`MemoryContextProvider` is another candidate because its file-backed path combines `MemoryFileStore` state with
|
||||
transcript files and still exposes `history_dumps` / `history_loads` passthroughs to the deprecated
|
||||
`FileHistoryProvider` codec hooks.
|
||||
|
||||
The follow-up should measure real framework payloads before changing formats, preserve compatibility or define a clear
|
||||
migration path for existing files, and consider whether each store needs readable JSON, compact binary storage, append
|
||||
semantics, or atomic whole-file replacement. Other candidates include file-backed todo state, but each should be
|
||||
evaluated independently rather than adopting msgspec by default solely for consistency.
|
||||
@@ -0,0 +1,48 @@
|
||||
# AGENTS.md
|
||||
|
||||
Instructions for AI coding agents working on durable agents documentation.
|
||||
|
||||
## Scope
|
||||
|
||||
This directory contains feature documentation for the durable agents integration. The source code and samples live elsewhere:
|
||||
|
||||
- .NET implementation: `dotnet/src/Microsoft.Agents.AI.DurableTask/` and `dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/`
|
||||
- Python implementation: `python/packages/durabletask/` and `python/packages/azurefunctions/` (package `agent-framework-azurefunctions`)
|
||||
- .NET samples: `dotnet/samples/04-hosting/DurableAgents/`
|
||||
- Python samples: `python/samples/04-hosting/durabletask/`
|
||||
- Official docs (Microsoft Learn): <https://learn.microsoft.com/agent-framework/integrations/azure-functions>
|
||||
|
||||
## Document structure
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `README.md` | Main technical overview: architecture, hosting models, orchestration patterns, and links to samples. |
|
||||
| `durable-agents-ttl.md` | Deep-dive on session Time-To-Live (TTL) configuration and behavior. |
|
||||
|
||||
Add new sibling documents when a topic is too detailed for the README (e.g., a new feature like reliable streaming or MCP tool exposure). Keep the README focused on orientation and link out to siblings for depth.
|
||||
|
||||
## Writing guidelines
|
||||
|
||||
- **Audience**: Developers already familiar with the Microsoft Agent Framework who want to understand what durability adds and how to use it.
|
||||
- **Host-agnostic first**: Durable agents work in console apps, Azure Functions, and any Durable Task–compatible host. Show host-agnostic patterns (plain orchestration functions, `IServiceCollection` registration) before Azure Functions–specific patterns. Avoid giving the impression that Azure Functions is the only hosting option.
|
||||
- **Both languages**: Always include C# and Python examples side by side. Keep them equivalent in functionality.
|
||||
- **Callout syntax**: Use GitHub-flavored callouts (`> [!NOTE]`, `> [!IMPORTANT]`, `> [!WARNING]`) rather than bold-text callouts (`> **Note:** ...`).
|
||||
- **Line length**: Do not wrap long lines. Rely on text viewers / renderers for line wrapping.
|
||||
- **Tables**: Use spaces around pipes in separator rows (`| --- |` not `|---|`).
|
||||
- **Code snippets**: Keep them minimal and self-contained. Omit boilerplate (using statements, environment variable reads) unless the snippet is specifically about setup.
|
||||
- **Cross-references**: Link to Microsoft Learn for conceptual background (Durable Entities, Durable Task Scheduler, Azure Functions). Link to sibling docs within this directory for feature deep-dives.
|
||||
|
||||
## Linting
|
||||
|
||||
Run markdownlint on all documents before committing, with line-length checks disabled:
|
||||
|
||||
```bash
|
||||
markdownlint docs/features/durable-agents/ --disable MD013
|
||||
```
|
||||
|
||||
## When to update these docs
|
||||
|
||||
- A new durable agent feature is added (e.g., a new orchestration pattern, hosting model, or configuration option).
|
||||
- The public API surface changes in a way that affects how developers use durable agents.
|
||||
- New sample directories are added — update the sample links in README.md.
|
||||
- The official Microsoft Learn documentation is restructured — update external links.
|
||||
@@ -1,9 +1,239 @@
|
||||
# Durable Agents Have Moved
|
||||
# Durable agents
|
||||
|
||||
Durable Task and Azure Functions integrations for Microsoft Agent Framework are now maintained in the [Durable Agent Framework extension repository](https://github.com/microsoft/agent-framework-durable-extension).
|
||||
## Overview
|
||||
|
||||
- [.NET source](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/src)
|
||||
- [.NET samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples)
|
||||
- [Python source](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/packages)
|
||||
- [Python samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples)
|
||||
- [Durable agent documentation](https://github.com/microsoft/agent-framework-durable-extension/tree/main/docs/features/durable-agents)
|
||||
Durable agents extend the standard Microsoft Agent Framework with **durable state management** powered by the Durable Task framework. An ordinary Agent Framework agent runs in-process: its conversation history lives in memory and is lost when the process ends. A durable agent persists conversation history and execution state in external storage so that sessions survive process restarts, failures, and scale-out events.
|
||||
|
||||
| Capability | Ordinary agent | Durable agent |
|
||||
| --- | --- | --- |
|
||||
| Conversation history | In-memory only | Durably persisted |
|
||||
| Failure recovery | State lost on crash | Automatically resumed |
|
||||
| Multi-instance scale-out | Not supported | Any worker can resume a session |
|
||||
| Multi-agent orchestrations | Manual coordination | Deterministic, checkpointed workflows |
|
||||
| Human-in-the-loop | Must keep process alive | Can wait days/weeks with zero compute |
|
||||
| Hosting | Any process | Console app, Azure Functions, or any Durable Task–compatible host |
|
||||
|
||||
> [!NOTE]
|
||||
> For a step-by-step tutorial and deployment guidance, see [Azure Functions (Durable)](https://learn.microsoft.com/agent-framework/integrations/azure-functions) on Microsoft Learn.
|
||||
|
||||
## How durable agents work
|
||||
|
||||
Durable agents are implemented on top of [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities) (also called "virtual actors"). Each **agent session** maps to one entity instance whose state contains the full conversation history. When you send a message to a durable agent, the following happens:
|
||||
|
||||
1. The message is dispatched to the entity identified by an `AgentSessionId` (a composite of the agent name and a unique session key).
|
||||
2. The entity loads its persisted `DurableAgentState`, which includes the complete conversation history.
|
||||
3. The entity invokes the underlying `AIAgent` with the full conversation history, collects the response, and appends both the request and the response to the state.
|
||||
4. The updated state is persisted back to durable storage automatically.
|
||||
|
||||
Because the entity framework serializes access to each entity instance, concurrent messages to the same session are processed one at a time, eliminating race conditions.
|
||||
|
||||
### Agent session identity
|
||||
|
||||
Every durable agent session is identified by an `AgentSessionId`, which has two components:
|
||||
|
||||
- **Name** – the registered name of the agent (case-insensitive).
|
||||
- **Key** – a unique session key (case-sensitive), typically a GUID.
|
||||
|
||||
The session ID is mapped to an underlying Durable Task entity ID with a `dafx-` prefix (e.g., `dafx-joker`). This naming convention is consistent across both .NET and Python implementations.
|
||||
|
||||
## Architecture
|
||||
|
||||
### .NET
|
||||
|
||||
The .NET implementation consists of two NuGet packages:
|
||||
|
||||
| Package | Purpose |
|
||||
| --- | --- |
|
||||
| `Microsoft.Agents.AI.DurableTask` | Core durable agent types: `DurableAIAgent`, `AgentEntity`, `DurableAgentSession`, `AgentSessionId`, `DurableAgentsOptions`, and the state model. |
|
||||
| `Microsoft.Agents.AI.Hosting.AzureFunctions` | Azure Functions hosting integration: auto-generated HTTP endpoints, MCP tool triggers, entity function triggers, and the `ConfigureDurableAgents` extension method on `FunctionsApplicationBuilder`. |
|
||||
|
||||
Key types:
|
||||
|
||||
- **`DurableAIAgent`** – A subclass of `AIAgent` used *inside orchestrations*. Obtained via `context.GetAgent("agentName")`, it routes `RunAsync` calls through the orchestration's entity APIs so that each call is checkpointed.
|
||||
- **`DurableAIAgentProxy`** – A subclass of `AIAgent` used *outside orchestrations* (e.g., from HTTP triggers or console apps). It signals the entity via `DurableTaskClient` and polls for the response.
|
||||
- **`AgentEntity`** – The `TaskEntity<DurableAgentState>` that hosts the real agent. It loads the registered `AIAgent` by name, wraps it in an `EntityAgentWrapper`, feeds it the full conversation history, and persists the result.
|
||||
- **`DurableAgentSession`** – An `AgentSession` subclass that carries the `AgentSessionId`.
|
||||
- **`DurableAgentsOptions`** – Builder for registering agents and configuring TTL.
|
||||
|
||||
### Python
|
||||
|
||||
The core Python implementation is in the `agent-framework-durabletask` package (`python/packages/durabletask`). Azure Functions hosting (including `AgentFunctionApp`) is in the separate `agent-framework-azurefunctions` package (`python/packages/azurefunctions`).
|
||||
|
||||
Key types:
|
||||
|
||||
- **`DurableAIAgent`** – A generic proxy (`DurableAIAgent[TaskT]`) implementing `SupportsAgentRun`. Returns a `TaskT` from `run()` — either an `AgentResponse` (client context) or a `DurableAgentTask` (orchestration context, must be `yield`ed).
|
||||
- **`DurableAIAgentWorker`** – Wraps a `TaskHubGrpcWorker` and registers agents as durable entities via `add_agent()`.
|
||||
- **`DurableAIAgentClient`** – Wraps a `TaskHubGrpcClient` for external callers. `get_agent()` returns a `DurableAIAgent[AgentResponse]`.
|
||||
- **`DurableAIAgentOrchestrationContext`** – Wraps an `OrchestrationContext` for use inside orchestrations. `get_agent()` returns a `DurableAIAgent[DurableAgentTask]`.
|
||||
- **`AgentEntity`** – Platform-agnostic agent execution logic that manages state, invokes the agent, handles streaming, and calls response callbacks.
|
||||
|
||||
## Hosting models
|
||||
|
||||
### Azure Functions
|
||||
|
||||
The recommended production hosting model. A single call to `ConfigureDurableAgents` (C#) or `AgentFunctionApp` (Python) automatically:
|
||||
|
||||
- Registers agent entities with the Durable Task worker.
|
||||
- Generates HTTP endpoints at `/api/agents/{agentName}/run` for each registered agent.
|
||||
- Supports `thread_id` query parameter / JSON field and the `x-ms-thread-id` response header for session continuity.
|
||||
- Supports fire-and-forget via the `x-ms-wait-for-response: false` header (returns HTTP 202).
|
||||
- Optionally exposes agents as MCP tools.
|
||||
|
||||
**C# example:**
|
||||
|
||||
```csharp
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
|
||||
.Build();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
**Python example:**
|
||||
|
||||
```python
|
||||
app = AgentFunctionApp(agents=[agent])
|
||||
```
|
||||
|
||||
### Console apps / generic hosts
|
||||
|
||||
For self-hosted or non-serverless scenarios, register durable agents via `IServiceCollection.ConfigureDurableAgents` (.NET) or `DurableAIAgentWorker` (Python) with explicit Durable Task worker and client configuration.
|
||||
|
||||
**C# example:**
|
||||
|
||||
```csharp
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.ConfigureDurableAgents(
|
||||
options => options.AddAIAgent(agent),
|
||||
workerBuilder: b => b.UseDurableTaskScheduler(connectionString),
|
||||
clientBuilder: b => b.UseDurableTaskScheduler(connectionString));
|
||||
})
|
||||
.Build();
|
||||
```
|
||||
|
||||
**Python example:**
|
||||
|
||||
```python
|
||||
worker = DurableAIAgentWorker(TaskHubGrpcWorker(host_address="localhost:4001"))
|
||||
worker.add_agent(agent)
|
||||
worker.start()
|
||||
```
|
||||
|
||||
## Deterministic multi-agent orchestrations
|
||||
|
||||
Durable agents can be composed into deterministic, checkpointed workflows using Durable Task orchestrations. The orchestration framework replays orchestrator code on failure, so completed agent calls are not re-executed.
|
||||
|
||||
### Patterns
|
||||
|
||||
| Pattern | Description |
|
||||
| --- | --- |
|
||||
| **Sequential (chaining)** | Call agents one after another, passing outputs forward. |
|
||||
| **Parallel (fan-out/fan-in)** | Run multiple agents concurrently and aggregate results. |
|
||||
| **Conditional** | Branch orchestration logic based on structured agent output. |
|
||||
| **Human-in-the-loop** | Pause for external events (approvals, feedback) with optional timeouts. |
|
||||
|
||||
### Using agents in orchestrations
|
||||
|
||||
Inside an orchestration function, obtain a `DurableAIAgent` via the orchestration context. Each agent gets its own session (created with `CreateSessionAsync` / `create_session`), and you can call the same agent multiple times on the same session to maintain conversation context across sequential invocations.
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
static async Task<string> WritingOrchestration(TaskOrchestrationContext context)
|
||||
{
|
||||
// Get a durable agent reference — works in any host (console app, Azure Functions, etc.)
|
||||
DurableAIAgent writer = context.GetAgent("WriterAgent");
|
||||
|
||||
// Create a session to maintain conversation context across multiple calls
|
||||
AgentSession session = await writer.CreateSessionAsync();
|
||||
|
||||
// First call: generate an initial draft
|
||||
AgentResponse<TextResponse> draft = await writer.RunAsync<TextResponse>(
|
||||
message: "Write a concise inspirational sentence about learning.",
|
||||
session: session);
|
||||
|
||||
// Second call: refine the draft — the agent sees the full conversation history
|
||||
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
|
||||
message: $"Improve this further while keeping it under 25 words: {draft.Result.Text}",
|
||||
session: session);
|
||||
|
||||
return refined.Result.Text;
|
||||
}
|
||||
```
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
def writing_orchestration(context, _):
|
||||
agent_ctx = DurableAIAgentOrchestrationContext(context)
|
||||
|
||||
# Get a durable agent reference — works in any host (standalone worker, Azure Functions, etc.)
|
||||
writer = agent_ctx.get_agent("WriterAgent")
|
||||
|
||||
# Create a session to maintain conversation context across multiple calls
|
||||
session = writer.create_session()
|
||||
|
||||
# First call: generate an initial draft
|
||||
draft = yield writer.run(
|
||||
messages="Write a concise inspirational sentence about learning.",
|
||||
session=session,
|
||||
)
|
||||
|
||||
# Second call: refine the draft — the agent sees the full conversation history
|
||||
refined = yield writer.run(
|
||||
messages=f"Improve this further while keeping it under 25 words: {draft.text}",
|
||||
session=session,
|
||||
)
|
||||
|
||||
return refined.text
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> In .NET, `DurableAIAgent.RunAsync<T>` deliberately avoids `ConfigureAwait(false)` because the Durable Task Framework uses a custom synchronization context — all continuations must run on the orchestration thread.
|
||||
|
||||
## Streaming and response callbacks
|
||||
|
||||
Durable agents do not support true end-to-end streaming because entity operations are request/response. However, **reliable streaming** is supported via response callbacks:
|
||||
|
||||
- **`IAgentResponseHandler`** (.NET) or **`AgentResponseCallbackProtocol`** (Python) – Implement this interface to receive streaming updates as the underlying agent generates them (e.g., push tokens to a Redis Stream for client consumption).
|
||||
- The entity still returns the complete `AgentResponse` after the stream is fully consumed.
|
||||
- Clients can reconnect and resume reading from a cursor-based stream (e.g., Redis Streams) without losing messages.
|
||||
|
||||
See the **Reliable Streaming** samples for a complete implementation using Redis Streams.
|
||||
|
||||
## Session TTL (Time-To-Live)
|
||||
|
||||
Durable agent sessions support automatic cleanup via configurable TTL. See [Session TTL](durable-agents-ttl.md) for details on configuration, behavior, and best practices.
|
||||
|
||||
## Observability
|
||||
|
||||
When using the [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) as the durable backend, you get built-in observability through its dashboard:
|
||||
|
||||
- **Conversation history** – View complete chat history for each agent session.
|
||||
- **Orchestration visualization** – See multi-agent execution flows, including parallel branches and conditional logic.
|
||||
- **Performance metrics** – Monitor agent response times, token usage, and orchestration duration.
|
||||
- **Debugging** – Trace tool invocations and external event handling.
|
||||
|
||||
## Samples
|
||||
|
||||
- **.NET** – [Console app samples](../../../dotnet/samples/04-hosting/DurableAgents/ConsoleApps/) and [Azure Functions samples](../../../dotnet/samples/04-hosting/DurableAgents/AzureFunctions/) covering single-agent, chaining, concurrency, conditionals, human-in-the-loop, long-running tools, MCP tool exposure, and reliable streaming.
|
||||
- **Python** – [Durable Task samples](../../../python/samples/04-hosting/durabletask/) covering single-agent, multi-agent, streaming, chaining, concurrency, conditionals, and human-in-the-loop.
|
||||
|
||||
## Packages
|
||||
|
||||
| Language | Package | Source |
|
||||
| --- | --- | --- |
|
||||
| .NET | `Microsoft.Agents.AI.DurableTask` | [`dotnet/src/Microsoft.Agents.AI.DurableTask`](../../../dotnet/src/Microsoft.Agents.AI.DurableTask) |
|
||||
| .NET | `Microsoft.Agents.AI.Hosting.AzureFunctions` | [`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions`](../../../dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions) |
|
||||
| Python | `agent-framework-durabletask` | [`python/packages/durabletask`](../../../python/packages/durabletask) |
|
||||
| Python | `agent-framework-azurefunctions` | [`python/packages/azurefunctions`](../../../python/packages/azurefunctions) |
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Azure Functions (Durable) — Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/azure-functions)
|
||||
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler)
|
||||
- [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities)
|
||||
- [Session TTL](durable-agents-ttl.md)
|
||||
|
||||
@@ -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.
|
||||
@@ -172,7 +172,7 @@ parsing a structured payload into a typed record), without coupling the holder t
|
||||
- Authorize and bind the candidate id to the authenticated principal/tenant before using it as an
|
||||
`AgentSessionStore` key or a workflow checkpoint session id.
|
||||
- For multi-user hosts, wrap the store with `IsolationKeyScopedAgentSessionStore` (for example via
|
||||
`UseClaimsBasedAgentIsolation(...)`), so the session namespace is scoped per principal.
|
||||
`UseClaimsBasedSessionIsolation(...)`), so the session namespace is scoped per principal.
|
||||
- Persist session/checkpoint state only after the run or stream has completed.
|
||||
|
||||
## E2E Code Samples
|
||||
|
||||
@@ -120,10 +120,6 @@ Code-reading landmarks:
|
||||
- `_process_model_function_calls(...)` handles only calls from a completed model response.
|
||||
- `_try_execute_function_calls(...)` decides approval/declaration/execution behavior for a batch.
|
||||
- `_replace_approval_contents_with_results(...)` is the occurrence-aware approval transcript normalizer.
|
||||
- `FunctionInvocationLayer._update_function_invocation_continuation_state(...)` updates continuation state after
|
||||
every service response. Provider layers may override it to carry provider-specific continuation metadata into
|
||||
the next service call, but must delegate to the base implementation so generic conversation continuation remains
|
||||
synchronized with the active `AgentSession`.
|
||||
|
||||
### Approval pause and resume
|
||||
|
||||
@@ -335,8 +331,6 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
- Service-managed continuation may omit inline reasoning/call items only when the hosted service already owns them.
|
||||
- Missing non-reconstructable reasoning fails explicitly before a provider request instead of silently dropping the
|
||||
content.
|
||||
- Foundry clients do not request `reasoning.encrypted_content` implicitly; callers may opt in explicitly when the
|
||||
selected deployment supports encrypted reasoning.
|
||||
- Compaction preserves or excludes the complete reasoning/call/result group atomically.
|
||||
|
||||
### Approval request and resume
|
||||
@@ -359,12 +353,6 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
- `function_approval_request` and `function_approval_response` are control-plane contents, not durable model
|
||||
transcript items.
|
||||
- A current hosted approval response must be sent once on the immediate resume request.
|
||||
- AG-UI removes a local approval response from its request and snapshot replay when a terminal result belongs to an
|
||||
already-consumed occurrence, including result-before-response replay. A client-authored result in the occurrence
|
||||
that is still registered as pending does not prove completion: AG-UI removes that result, keeps the validated
|
||||
response for local execution, and leaves hosted approval responses as provider protocol data.
|
||||
- Hosted AG-UI approval interrupts expose an accept/reject decision only; argument edits are rejected because the
|
||||
hosted provider executes the server-owned request rather than client-edited arguments.
|
||||
- A server-issued approval request must not be replayed inline during service-side continuation.
|
||||
- History providers may retain approval control contents in their backing store for audit, but base history replay
|
||||
filters them before later model calls.
|
||||
@@ -376,8 +364,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
- Model-bound history contains one function call/result pair per completed logical occurrence.
|
||||
- Append-only history must not replay stale approval request/response wrappers to the model.
|
||||
- Framework-managed and service-managed continuation must preserve the same logical call/result transcript.
|
||||
- A trusted terminal result consumes the corresponding approval authority in explicit stateless replay; a result in a
|
||||
server-registered pending occurrence cannot consume that authority before local execution.
|
||||
- A terminal result consumes the corresponding approval authority in explicit stateless replay.
|
||||
|
||||
## Scenario-to-test matrix
|
||||
|
||||
@@ -393,7 +380,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
| Calls across response messages | Every actionable call is executed once. | `test_base_client_executes_function_calls_across_multiple_response_messages` |
|
||||
| Parallel calls | Results retain the corresponding call ids and execution count. | `test_max_function_calls_limits_parallel_invocations`, `test_streaming_multiple_function_calls_parallel_execution` |
|
||||
| Informational-only call | The call is returned but not executed or approved. | `test_informational_only_function_call_is_not_invoked`, `test_informational_only_function_call_does_not_request_approval`, `test_streaming_informational_only_function_call_is_not_invoked` |
|
||||
| Declaration-only call | The call is surfaced as user input and is not executed; streaming arguments appear once while finalized request metadata remains available. | `test_declaration_only_tool`, `test_streaming_declaration_only_tool_preserves_metadata_without_duplicate_arguments` |
|
||||
| Declaration-only call | The call is surfaced as user input and is not executed. | `test_declaration_only_tool` |
|
||||
| Function invocation disabled | The client bypasses the invocation loop without losing invocation kwargs. | `test_function_invocation_config_enabled_false`, `test_function_invocation_config_enabled_false_preserves_invocation_kwargs`, `test_streaming_function_invocation_config_enabled_false` |
|
||||
| Runtime tool changes | Added tools become available on the next iteration and retain approval behavior. | `test_add_tools_available_next_iteration`, `test_add_tools_with_approval_required_tool` |
|
||||
|
||||
@@ -451,8 +438,6 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
| Standing tool rule | Tool-level approval applies only to later matching tools. | `test_tool_approval_middleware_always_approve_tool_rule` |
|
||||
| Hosted server boundary | Standing approval does not cross `server_label`. | `test_tool_approval_middleware_standing_rules_include_hosted_server_boundary` |
|
||||
| Argument-scoped rule | Exact arguments are required; empty arguments are not tool-wide. | `test_tool_approval_middleware_always_approve_tool_with_arguments_rule`, `test_tool_approval_middleware_empty_arguments_rule_is_not_tool_wide` |
|
||||
| Provider-injected approval tool | A tool added during `before_run` defers to in-run resolution, executes once, and emits one result. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes` |
|
||||
| AG-UI provider boundary | Completed local approval controls from AG-UI request and snapshot replay are absent from raw chat-client input while deferred and hosted approvals keep their respective in-run/provider paths. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_does_not_forward_resolved_local_approval_control_to_chat_client`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_canonical_resume_preserves_hosted_approval_for_provider`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_removes_duplicate_completed_controls`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_pairs_reused_call_ids_by_occurrence`, `packages/ag-ui/tests/ag_ui/test_run.py::test_canonical_hosted_approval_resume_rejects_edited_arguments_without_mutating_pending` |
|
||||
|
||||
### Errors, control flow, and limits
|
||||
|
||||
@@ -467,7 +452,6 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
| Middleware termination | Normal non-approval loop stops without a second model call. | `test_terminate_loop_single_function_call`, `test_terminate_loop_multiple_function_calls_one_terminates`, `test_terminate_loop_streaming_single_function_call` |
|
||||
| Maximum iterations | No orphan calls; a final no-tool response or deterministic fallback is returned. | `test_max_iterations_limit`, `test_max_iterations_no_orphaned_function_calls`, `test_max_iterations_makes_final_toolchoice_none_call`, `test_max_iterations_blank_final_fallback_synthesizes_message`, streaming equivalents |
|
||||
| Maximum function calls | Parallel overshoot is bounded after the batch; every executed result group counts even without a `function_result`; blank final responses get fallback content. | `test_max_function_calls_limits_parallel_invocations`, `test_max_function_calls_single_calls_per_iteration`, `test_user_input_request_multiple_contents_propagate`, `test_approval_resume_user_input_counts_toward_function_call_budget`, `test_max_function_calls_blank_final_fallback_synthesizes_message`, streaming equivalent |
|
||||
| Provider tool content after an active limit | Locally actionable calls and local approval requests returned despite `tool_choice="none"` are removed in both response modes. Provider-executed informational call/result pairs, hosted approval requests, and metadata-only streaming updates remain visible; fallback text never replaces retained transcript content. | `test_function_invocation_limit_drops_unexecutable_tool_content`, `test_streaming_function_invocation_limit_drops_unexecutable_tool_content`, `test_streaming_function_invocation_limit_preserves_metadata_after_tool_content_is_dropped`, `test_function_invocation_limit_preserves_provider_executed_tool_pair`, `test_streaming_function_invocation_limit_preserves_provider_executed_tool_pair`, `test_function_invocation_limit_appends_fallback_after_provider_executed_tool_pair`, `test_streaming_function_invocation_limit_appends_fallback_after_provider_executed_tool_pair`, `test_function_invocation_limit_preserves_hosted_approval_request`, `test_streaming_function_invocation_limit_preserves_hosted_approval_request` |
|
||||
| Conversation continuation | Conversation id updates between iterations and is cleared on stop where required. | `test_conversation_id_updated_in_options_between_tool_iterations`, `test_function_invocation_stop_clears_conversation_id_non_stream`, `test_streaming_function_invocation_stop_clears_conversation_id` |
|
||||
|
||||
### History and provider serialization
|
||||
@@ -478,13 +462,10 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
| Pending placeholder history | An approval response remains replayable while its only result is `[APPROVAL_PENDING]`. | `packages/core/tests/core/test_sessions.py::test_filter_approval_controls_keeps_response_for_pending_placeholder` |
|
||||
| Pending hosted history replay | Stateless hosted approval requests remain replayable until a response is recorded, then both controls become inert. | `packages/openai/tests/openai/test_openai_chat_client.py::test_stateless_history_preserves_pending_hosted_approval_request_until_response` |
|
||||
| Non-history provider plus session | Local history is still auto-injected for approval resume. | `packages/core/tests/core/test_agents.py::test_non_history_context_provider_still_injects_inmemory` |
|
||||
| Hosted per-service-call persistence | A host-managed transcript remains available throughout a local function-call loop without being persisted into the framework session and replayed on the next hosted request. | `packages/foundry_hosting/tests/test_responses.py::TestAgentSessionPersistence::test_per_service_call_persistence_preserves_function_loop_history` |
|
||||
| Service-side approval decision | Stored hosted request is skipped; the current approved or rejected hosted response is sent, while local approval controls are omitted from provider input. | `packages/openai/tests/openai/test_openai_chat_client.py::test_prepare_messages_strips_approval_request_but_keeps_response_under_storage`, `test_prepare_messages_drops_local_approval_controls` |
|
||||
| OpenAI approval serialization | Hosted approval id and decision serialize to `mcp_approval_response`; local approvals remain in-process. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` |
|
||||
| OpenAI approval serialization | Approval id and decision serialize to `mcp_approval_response`. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` |
|
||||
| OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` |
|
||||
| Stored function call/result | Service-side storage drops server-issued calls but keeps new outputs. | `test_prepare_options_with_conversation_id_strips_server_issued_items`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
|
||||
| Stateless reasoning replay | Replay reconstructs reasoning, call, and result together; missing required reasoning fails before the request. | `test_tool_loop_store_false_replays_encrypted_reasoning_group`, `test_stateless_request_rejects_non_replayable_reasoning_bound_mcp_output`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
|
||||
| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning`, `packages/foundry_hosting/tests/test_responses_int.py::TestReasoningHostedMcpReplay::test_second_turn_replays_mcp_call_with_encrypted_reasoning` |
|
||||
| Opaque reasoning signature replay | Provider-specific opaque reasoning metadata is captured and restored on reconstructed calls. | `packages/gemini/tests/test_gemini_client.py::test_function_call_part_captures_thought_signature_as_reasoning_content`, `test_reconstructed_function_call_replays_thought_signature_from_reasoning_content` |
|
||||
| Chat Completions approval wrappers | Framework approval wrappers are not sent as chat messages. | `packages/openai/tests/openai/test_openai_chat_completion_client.py` approval serialization tests |
|
||||
| AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_resume_emits_tool_call_result`, `test_approval_resume_result_has_content`, `test_approval_resume_snapshot_replaces_approval_payload_with_tool_result`, `test_approval_resume_zero_updates_emits_tool_result` |
|
||||
@@ -492,9 +473,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
|
||||
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `test_resolve_approval_responses_preserves_follow_up_user_input_group` |
|
||||
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `test_resolve_approval_responses_returns_failure_when_grouped_execution_raises` |
|
||||
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `test_no_approval_no_extra_tool_result` |
|
||||
| AG-UI `confirm_changes` snapshot | An accepted synthetic confirmation is replaced only when its original function call has a real result; rejection is cleaned explicitly, and missing accepted results remain inert. | `packages/ag-ui/tests/ag_ui/test_confirm_changes_snapshot.py` |
|
||||
| AG-UI malformed `confirm_changes` metadata | Non-list tool-call metadata and malformed argument JSON are ignored without guessing a target call. | `test_confirm_changes_target_ignores_non_list_tool_calls`, `test_confirm_changes_target_rejects_malformed_arguments_json` |
|
||||
| Compaction pair integrity | Adjacent and non-adjacent pairs, including assistant-embedded results and completed reused-id occurrences, remain atomic without pairing ambiguous or out-of-order ids. | `packages/core/tests/core/test_compaction.py::test_group_annotations_keep_tool_call_and_tool_result_atomic`, `test_group_annotations_include_reasoning_in_tool_call_group`, `test_group_annotations_pair_nonadjacent_function_result_by_call_id`, `test_group_annotations_pair_multiple_nonadjacent_results_with_declaration`, `test_group_annotations_pair_completed_reused_call_id_occurrences`, `test_group_annotations_close_assistant_embedded_result_before_reused_call_id`, `test_sliding_window_does_not_retain_orphan_result_after_assistant_embedded_result`, `test_sliding_window_keeps_reused_call_id_occurrences_atomic`, `test_group_annotations_do_not_pair_ambiguous_duplicate_call_ids` |
|
||||
| Compaction pair integrity | Function call/result groups remain atomic. | `packages/core/tests/core/test_compaction.py::test_group_annotations_keep_tool_call_and_tool_result_atomic`, `test_group_annotations_include_reasoning_in_tool_call_group` |
|
||||
|
||||
## Required coverage gaps
|
||||
|
||||
@@ -502,7 +481,13 @@ These scenarios are required but are not fully covered by merged tests on `main`
|
||||
|
||||
| Gap | Tracking |
|
||||
|---|---|
|
||||
| Non-adjacent and reused-id call/result occurrences remain atomic during compaction. | #7212 |
|
||||
| Provider-injected approval-required tools defer until `before_run` tools exist and still emit one result. | #7043 |
|
||||
| Service-side storage sends the current approval response while omitting the stored request. | #7125 |
|
||||
| Service-owned `previous_response_id` continuation cannot execute a terminal approval again on a later turn. | #6851 |
|
||||
| A provider that ignores `tool_choice="none"` after an invocation limit cannot expose an unanswered call. | #7045 |
|
||||
| Declaration-only streaming preserves request metadata without duplicating arguments. | #6973 |
|
||||
| AG-UI `confirm_changes` cleanup correlates one result by original function call id when several results exist. | #6828 |
|
||||
|
||||
Do not mark these rows covered by nearby tests; each needs a dedicated regression at the owning layer.
|
||||
|
||||
@@ -551,12 +536,6 @@ Before accepting an update, reviewers must confirm:
|
||||
|
||||
- #7241 — approval-resolution result streaming
|
||||
- #7267 / #7271 and #7304 — replayed calls and reused ids
|
||||
- #7043 — provider-injected approval execution
|
||||
- #6828 — AG-UI `confirm_changes` snapshot correlation
|
||||
- #7212 — non-adjacent and reused-id compaction integrity
|
||||
- #7125 — service-side approval response serialization
|
||||
- #7045 — post-limit tool-content transcript integrity
|
||||
- #6973 — declaration-only streaming metadata and argument integrity
|
||||
- #6851 — duplicate side effects after approval continuation
|
||||
- #7383 — bind approval responses to framework-issued requests after this foundation merges
|
||||
- #6963 / #7095 — opaque reasoning-signature replay
|
||||
|
||||
@@ -135,8 +135,7 @@ only to approved first-party endpoints.
|
||||
| 14 | `core.file_skills_source` | File-backed skills | `agent_framework.FileSkillsSource` |
|
||||
| 15 | `core.in_memory_skills_source` | In-memory / programmatic skills | `agent_framework.InMemorySkillsSource` |
|
||||
| 16 | `core.mcp_skills_source` | MCP-backed skills | `agent_framework.MCPSkillsSource` |
|
||||
| 17 | `core.session_store` | Agent session store | `agent_framework.SessionStore` / `FileSessionStore` |
|
||||
| 18–31 | _reserved_ | core growth | — |
|
||||
| 17–31 | _reserved_ | core growth | — |
|
||||
| 32 | `orchestration.sequential` | Sequential orchestration | `agent_framework_orchestrations.SequentialBuilder` |
|
||||
| 33 | `orchestration.concurrent` | Concurrent orchestration | `agent_framework_orchestrations.ConcurrentBuilder` |
|
||||
| 34 | `orchestration.group_chat` | Group-chat orchestration | `agent_framework_orchestrations.GroupChatBuilder` |
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
|
||||
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
||||
<!-- Microsoft.Azure.* -->
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.61.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
|
||||
<!-- Newtonsoft.Json -->
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- System.* -->
|
||||
@@ -104,7 +104,6 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="10.7.0" />
|
||||
<!-- Vector Stores -->
|
||||
<PackageVersion Include="CommunityToolkit.VectorData.CosmosNoSql" Version="1.0.0" />
|
||||
<PackageVersion Include="CommunityToolkit.VectorData.InMemory" Version="1.0.0" />
|
||||
<PackageVersion Include="CommunityToolkit.VectorData.Qdrant" Version="1.0.0" />
|
||||
<!-- Agent SDKs -->
|
||||
@@ -135,6 +134,22 @@
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.2.4.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.8.1" />
|
||||
<!-- Durable Task -->
|
||||
<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.Extensions.DurableTask" Version="1.12.1" />
|
||||
<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" />
|
||||
<!-- Valkey -->
|
||||
<!-- Redis -->
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
|
||||
<!-- Valkey -->
|
||||
<PackageVersion Include="Valkey.Glide" Version="1.1.0" />
|
||||
<!-- Console UX -->
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<Project Path="samples/01-get-started/03_multi_turn/03_multi_turn.csproj" />
|
||||
<Project Path="samples/01-get-started/04_memory/04_memory.csproj" />
|
||||
<Project Path="samples/01-get-started/05_first_workflow/05_first_workflow.csproj" />
|
||||
<Project Path="samples/01-get-started/06_host_your_agent/06_host_your_agent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/">
|
||||
<File Path="samples/02-agents/README.md" />
|
||||
@@ -72,6 +73,24 @@
|
||||
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
|
||||
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/DurableWorkflows/" />
|
||||
<Folder Name="/Samples/04-hosting/DurableWorkflows/ConsoleApps/">
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/DurableWorkflows/AzureFunctions/">
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
</Folder>
|
||||
@@ -186,8 +205,6 @@
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory/AgentWithMemory_Step05_BoundedChatHistory.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/AgentWithMemory_Step08_MemoryUsingCosmosNoSql.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
|
||||
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
|
||||
@@ -383,6 +400,29 @@
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/" />
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
||||
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
|
||||
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/README.md" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/ConsoleApps/">
|
||||
<File Path="samples/04-hosting/DurableAgents/ConsoleApps/README.md" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/A2A/">
|
||||
<File Path="samples/02-agents/A2A/README.md" />
|
||||
<Project Path="samples/02-agents/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
|
||||
@@ -592,6 +632,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
@@ -600,6 +641,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||
@@ -628,8 +670,10 @@
|
||||
<Project Path="tests/Foundry.Hosting.IntegrationTests.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.csproj" />
|
||||
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.IntegrationTests/Microsoft.Agents.AI.Hyperlight.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.IntegrationTests/Microsoft.Agents.AI.Tools.Shell.IntegrationTests.csproj" />
|
||||
@@ -647,12 +691,14 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
|
||||
@@ -671,3 +717,4 @@
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
"src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
|
||||
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
|
||||
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
|
||||
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
|
||||
|
||||
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.AspNetCore\\Microsoft.Agents.AI.Hosting.AspNetCore.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
|
||||
"src\\Microsoft.Agents.AI.LocalCodeAct\\Microsoft.Agents.AI.LocalCodeAct.csproj",
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
<ItemGroup Condition="'$(InjectSharedDiagnosticIds)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\DiagnosticIds\*.cs" LinkBase="Shared\DiagnosticIds" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedUsage)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Usage\*.cs" LinkBase="Shared\Usage" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
When specified, only test projects whose filename matches this pattern are kept.
|
||||
|
||||
.PARAMETER TestProjectNameExcludeFilter
|
||||
Optional wildcard pattern(s) to exclude test projects by name (e.g., *Slow.IntegrationTests*).
|
||||
Optional wildcard pattern(s) to exclude test projects by name (e.g., *DurableTask.IntegrationTests*).
|
||||
When specified, test projects whose filename matches any of these patterns are removed.
|
||||
Applied after TestProjectNameIncludeFilter. Can be a single string or an array of strings.
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
dotnet test --solution (./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472) --no-build -f net472
|
||||
|
||||
.EXAMPLE
|
||||
# Generate integration tests while excluding a long-running test project
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameIncludeFilter "*IntegrationTests*" -TestProjectNameExcludeFilter "*Slow.IntegrationTests*" -OutputPath filtered-integration.slnx
|
||||
# Generate integration tests excluding DurableTask and AzureFunctions
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameIncludeFilter "*IntegrationTests*" -TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" -OutputPath filtered-other-integration.slnx
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
|
||||
@@ -510,48 +510,6 @@ internal static class AgentsSamples
|
||||
SkipReason = "Requires a running Neo4j instance; standalone sample outside the repo's CPM build.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "AgentWithMemory_Step07_FileMemoryProvider",
|
||||
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
MustContain =
|
||||
[
|
||||
"Memory files will be written to:",
|
||||
"=== First conversation ===",
|
||||
"=== Memory files on disk ===",
|
||||
"=== Second conversation (new session) ===",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should acknowledge that the user is vegetarian and travels with a dog, indicating the agent stored these preferences.",
|
||||
"The memory files section should list at least one memory file written by the agent, such as a file about the user's preferences.",
|
||||
"The second conversation should recommend a hotel and a restaurant in Paris that are consistent with the remembered preferences, for example a pet-friendly hotel and a restaurant with vegetarian options, even though it is a new session.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "AgentWithMemory_Step08_MemoryUsingCosmosNoSql",
|
||||
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "COSMOS_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "COSMOS_DATABASE_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"First session:",
|
||||
"Second session (recalling prior chat history from Cosmos DB):",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain two joke responses.",
|
||||
"The first joke should be about a pirate (as explicitly requested).",
|
||||
"The second joke should also be pirate-themed or similar to what the user likes, since chat history from the first session should be recalled from Cosmos DB.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ── AgentWithRAG ────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
|
||||
@@ -92,5 +92,14 @@ internal static class GetStartedSamples
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "06_host_your_agent",
|
||||
ProjectPath = "samples/01-get-started/06_host_your_agent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.17.0</VersionPrefix>
|
||||
<VersionPrefix>1.15.0</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<DateSuffix>260804</DateSuffix>
|
||||
<DateSuffix>260722</DateSuffix>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.17.0</GitTag>
|
||||
<GitTag>1.15.0</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>HostedAgent</AssemblyName>
|
||||
<RootNamespace>HostedAgent</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.Identity" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to host an AI agent with Azure Functions (DurableAgents).
|
||||
//
|
||||
// Prerequisites:
|
||||
// - Azure Functions Core Tools
|
||||
// - Foundry project endpoint and credentials
|
||||
//
|
||||
// Environment variables:
|
||||
// FOUNDRY_PROJECT_ENDPOINT
|
||||
// FOUNDRY_MODEL (defaults to "gpt-5.4-mini")
|
||||
//
|
||||
// Run with: func start
|
||||
// Then call: POST http://localhost:7071/api/agents/HostedAgent/run
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: model, instructions: "You are a helpful assistant hosted in Azure Functions.", name: "HostedAgent");
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
// This will automatically generate HTTP API endpoints for the agent.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -1,3 +0,0 @@
|
||||
# Azure Functions Hosting Sample Has Moved
|
||||
|
||||
The Azure Functions hosting tutorial is now maintained as the [single-agent Durable Agent sample](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples/DurableAgents/AzureFunctions/01_SingleAgent).
|
||||
@@ -228,7 +228,7 @@ dotnet run
|
||||
|
||||
`ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace.
|
||||
|
||||
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedAgentIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
|
||||
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ builder.Services.AddAGUIServer();
|
||||
builder.WebHost.UseUrls("http://localhost:8888");
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@
|
||||
<ItemGroup>
|
||||
<!-- AgentMemory (published) — an unofficial .NET port of the Neo4j Labs agent-memory library + its
|
||||
Microsoft Agent Framework adapter. -->
|
||||
<PackageReference Include="AgentMemory" Version="1.3.0" />
|
||||
<PackageReference Include="AgentMemory.AgentFramework" Version="1.3.0" />
|
||||
<PackageReference Include="AgentMemory" Version="1.2.0" />
|
||||
<PackageReference Include="AgentMemory.AgentFramework" Version="1.2.0" />
|
||||
<!-- Microsoft Agent Framework (matches AgentMemory's target) + the OpenAI/Foundry chat & embedding clients. -->
|
||||
<PackageReference Include="Microsoft.Agents.AI" Version="1.9.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" />
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to give an agent file-based memory using the FileMemoryProvider.
|
||||
// The FileMemoryProvider exposes a set of tools to the agent (write, read, delete, list, grep and replace)
|
||||
// that allow it to store memories as individual files in an AgentFileStore.
|
||||
// Because the files are stored outside of the conversation, the agent can recall them
|
||||
// in later conversations, even after the original chat history is gone.
|
||||
//
|
||||
// The sample also shows how to control the folder that memory files are written to,
|
||||
// by supplying a state initializer callback that sets the working folder for each session.
|
||||
|
||||
#pragma warning disable MAAI001 // AgentFileStore and its implementations are experimental.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// The id of the user that we are storing memories for.
|
||||
// It is used below to give each user their own memory folder.
|
||||
const string UserId = "UID1";
|
||||
|
||||
// Create the file store that the FileMemoryProvider will use to persist memory files.
|
||||
// Here we use a file system backed store rooted at a local folder called "agent-memory",
|
||||
// but any AgentFileStore implementation can be used, e.g. InMemoryAgentFileStore or a custom
|
||||
// implementation backed by blob storage.
|
||||
var memoryRoot = Path.Combine(AppContext.BaseDirectory, "agent-memory");
|
||||
var fileStore = new FileSystemAgentFileStore(memoryRoot);
|
||||
|
||||
// The working folder that memories for this user will be written to, relative to the store root.
|
||||
// The folder you choose determines the scope and lifetime of the memories:
|
||||
// - A stable folder, like the per-user one below, gives you durable memories that are shared by
|
||||
// every session for that user. That is what allows the second conversation further down to
|
||||
// recall what the user said in the first.
|
||||
// - A unique folder per session gives you memories that are isolated to a single session, e.g.
|
||||
// generate one in the state initializer callback below:
|
||||
// _ => new FileMemoryState { WorkingFolder = Guid.NewGuid().ToString() }
|
||||
var workingFolder = $"users/{UserId}";
|
||||
|
||||
Console.WriteLine($"Memory files will be written to: {Path.Combine(memoryRoot, workingFolder)}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create the file memory provider.
|
||||
// The second parameter is a state initializer callback that is invoked whenever the provider
|
||||
// cannot find existing state in a session, i.e. typically the first time it is used with a new session.
|
||||
// It allows us to configure the folder that memory files for that session are written to.
|
||||
// If no callback is supplied, the working folder defaults to the root of the store,
|
||||
// which means all sessions share a single, flat set of memory files.
|
||||
using var fileMemoryProvider = new FileMemoryProvider(
|
||||
fileStore,
|
||||
_ => new FileMemoryState { WorkingFolder = workingFolder });
|
||||
|
||||
// Create the agent and attach the FileMemoryProvider so that the agent gets the file memory tools.
|
||||
AIAgent agent = new AIProjectClient(
|
||||
new Uri(endpoint),
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
new DefaultAzureCredential())
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful travel assistant. Remember what the user tells you about themselves so that you can give better recommendations later."
|
||||
},
|
||||
Name = "TravelAssistant",
|
||||
AIContextProviders = [fileMemoryProvider],
|
||||
});
|
||||
|
||||
// First conversation: tell the agent something worth remembering.
|
||||
// The agent should use the file_memory_write tool to store it as a file in the working folder.
|
||||
AgentSession firstSession = await agent.CreateSessionAsync();
|
||||
Console.WriteLine("=== First conversation ===");
|
||||
Console.WriteLine(await agent.RunAsync(
|
||||
"I'm vegetarian and I always travel with my dog. Please remember this for future trips.",
|
||||
firstSession));
|
||||
Console.WriteLine();
|
||||
|
||||
// Show the memory files that the agent created on disk.
|
||||
Console.WriteLine("=== Memory files on disk ===");
|
||||
foreach (var file in Directory.EnumerateFiles(Path.Combine(memoryRoot, workingFolder)))
|
||||
{
|
||||
Console.WriteLine(Path.GetFileName(file));
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Second conversation: a brand new session with no chat history from the first conversation.
|
||||
// The provider surfaces the memory index to the agent, and the agent can read the memory files
|
||||
// using the file_memory_read tool, so it can still recall the user's preferences.
|
||||
AgentSession secondSession = await agent.CreateSessionAsync();
|
||||
Console.WriteLine("=== Second conversation (new session) ===");
|
||||
Console.WriteLine(await agent.RunAsync(
|
||||
"Suggest a hotel and a restaurant for my trip to Paris next week.",
|
||||
secondSession));
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
# File Based Memory with FileMemoryProvider
|
||||
|
||||
This sample demonstrates how to give an agent file-based memory using the `FileMemoryProvider`.
|
||||
|
||||
The `FileMemoryProvider` is an `AIContextProvider` that exposes a set of memory tools to the agent, allowing the agent to decide what to remember and when to recall it. Each memory is stored as an individual file in an `AgentFileStore`, so memories survive beyond the lifetime of a single conversation.
|
||||
|
||||
## Concepts
|
||||
|
||||
- **`FileMemoryProvider`**: An `AIContextProvider` that adds the following tools to the agent:
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `file_memory_write` | Write a memory file with a name, content and optional description. |
|
||||
| `file_memory_read` | Read the content of a memory file by name. |
|
||||
| `file_memory_delete` | Delete a memory file by name. |
|
||||
| `file_memory_ls` | List all memory files with their descriptions. |
|
||||
| `file_memory_grep` | Search memory file contents using a regular expression. |
|
||||
| `file_memory_replace` | Replace occurrences of a substring within a memory file. |
|
||||
| `file_memory_replace_lines` | Replace whole lines within a memory file. |
|
||||
|
||||
The provider also maintains a `memories.md` index file, which it injects into the conversation so the agent knows which memories are available without having to list them first.
|
||||
|
||||
- **`AgentFileStore`**: The pluggable storage abstraction used by the provider. This sample uses `FileSystemAgentFileStore` to store memories on the local disk, but `InMemoryAgentFileStore` or a custom implementation (e.g. backed by blob storage) can be used instead.
|
||||
|
||||
- **`FileMemoryState`**: The per-session state of the provider. Its `WorkingFolder` property determines the folder, relative to the store root, that memory files are written to.
|
||||
|
||||
## Configuring the memory folder
|
||||
|
||||
By default, all sessions share the root folder of the store, which means every session reads and writes the same flat set of memory files.
|
||||
|
||||
To scope memories, e.g. per user, per tenant or per session, pass a state initializer callback to the `FileMemoryProvider` constructor. The callback receives the `AgentSession` and is invoked whenever the provider cannot find existing state in that session, i.e. typically the first time the provider is used with a new session:
|
||||
|
||||
```csharp
|
||||
using var fileMemoryProvider = new FileMemoryProvider(
|
||||
fileStore,
|
||||
session => new FileMemoryState { WorkingFolder = $"users/{userId}" });
|
||||
```
|
||||
|
||||
In this sample, memories are written to `agent-memory/users/UID1` under the application's base directory. Because the folder is derived from a fixed user id rather than the session, a new session for the same user picks up the memories written by earlier sessions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- A Microsoft Foundry project with a chat model deployment
|
||||
- Run `az login` to authenticate with `DefaultAzureCredential`
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Your Foundry project endpoint | *(required)* |
|
||||
| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` |
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## How it Works
|
||||
|
||||
1. A `FileSystemAgentFileStore` is created, rooted at a local `agent-memory` folder.
|
||||
2. A `FileMemoryProvider` is created over that store, with a state initializer that puts the memories for the current user in their own working folder.
|
||||
3. The provider is attached to the agent via `ChatClientAgentOptions.AIContextProviders`, which gives the agent the `file_memory_*` tools and instructions for using them.
|
||||
4. In the first conversation, the user shares some preferences and the agent calls `file_memory_write` to store them as a file in the working folder. The sample then lists the files that were created on disk.
|
||||
5. In the second conversation, a brand new session is created with no chat history from the first conversation. The provider injects the memory index into the conversation, and the agent calls `file_memory_read` to recall the stored preferences when making its recommendations.
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="CommunityToolkit.VectorData.CosmosNoSql" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to persist chat history in Azure Cosmos DB for NoSQL using the ChatHistoryMemoryProvider.
|
||||
// The agent can then use chat history from prior conversations to inform responses in new conversations.
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using CommunityToolkit.VectorData.CosmosNoSql;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Azure.Cosmos;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
|
||||
var embeddingDimensions = 3072;
|
||||
if (Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_DIMENSIONS") is string embeddingDimensionsValue &&
|
||||
(!int.TryParse(embeddingDimensionsValue, out embeddingDimensions) || embeddingDimensions <= 0))
|
||||
{
|
||||
throw new InvalidOperationException("FOUNDRY_EMBEDDING_DIMENSIONS must be a positive integer.");
|
||||
}
|
||||
var cosmosEndpoint = Environment.GetEnvironmentVariable("COSMOS_ENDPOINT") ?? throw new InvalidOperationException("COSMOS_ENDPOINT is not set.");
|
||||
var cosmosDatabaseName = Environment.GetEnvironmentVariable("COSMOS_DATABASE_NAME") ?? "agent-memory";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
DefaultAzureCredential credential = new();
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
using CosmosClient cosmosClient = new(
|
||||
cosmosEndpoint,
|
||||
credential,
|
||||
new CosmosClientOptions
|
||||
{
|
||||
UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default,
|
||||
});
|
||||
|
||||
DatabaseResponse databaseResponse = await cosmosClient.CreateDatabaseIfNotExistsAsync(cosmosDatabaseName);
|
||||
|
||||
VectorStore vectorStore = new CosmosNoSqlVectorStore(
|
||||
databaseResponse.Database,
|
||||
new CosmosNoSqlVectorStoreOptions
|
||||
{
|
||||
JsonSerializerOptions = JsonSerializerOptions.Default,
|
||||
EmbeddingGenerator = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
.GetEmbeddingClient(embeddingDeploymentName)
|
||||
.AsIEmbeddingGenerator(),
|
||||
});
|
||||
|
||||
var userId = $"sample-{Guid.NewGuid():N}";
|
||||
|
||||
// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in Cosmos DB.
|
||||
AIAgent agent = aiProjectClient
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
AIContextProviders = [new ChatHistoryMemoryProvider(
|
||||
vectorStore,
|
||||
collectionName: "chathistory",
|
||||
vectorDimensions: embeddingDimensions,
|
||||
// Callback to configure the initial state of the ChatHistoryMemoryProvider.
|
||||
// The ChatHistoryMemoryProvider stores its state in the AgentSession and this callback
|
||||
// will be called whenever the ChatHistoryMemoryProvider cannot find existing state in the session,
|
||||
// typically the first time it is used with a new session.
|
||||
_ => new ChatHistoryMemoryProvider.State(
|
||||
// Configure the scope values under which chat messages will be stored.
|
||||
// In this case, we are using a per-run user ID and a unique session ID for each new session.
|
||||
storageScope: new() { UserId = userId, SessionId = Guid.NewGuid().ToString("N") },
|
||||
// Configure the scope which would be used to search for relevant prior messages.
|
||||
// In this case, we are searching for any messages for the user across all sessions.
|
||||
searchScope: new() { UserId = userId }))]
|
||||
});
|
||||
|
||||
// Start a new session for the agent conversation.
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the session that stores conversation history in Cosmos DB.
|
||||
Console.WriteLine("First session:");
|
||||
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session));
|
||||
|
||||
// Start a second session. Since we configured the search scope to be across all sessions for the user,
|
||||
// the agent should remember that the user likes pirate jokes.
|
||||
AgentSession session2 = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the second session.
|
||||
Console.WriteLine("Second session (recalling prior chat history from Cosmos DB):");
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2));
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Agent with Memory Using Azure Cosmos DB for NoSQL
|
||||
|
||||
This sample uses `ChatHistoryMemoryProvider` with `CosmosNoSqlVectorStore` to persist chat history in Azure Cosmos DB for NoSQL and recall relevant messages in a new agent session.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Authenticating to Microsoft Foundry and Azure Cosmos DB with `DefaultAzureCredential`
|
||||
- Storing chat messages in an Azure Cosmos DB vector store
|
||||
- Creating the configured database and chat-history container when they do not exist
|
||||
- Recalling relevant chat history across agent sessions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
2. A Microsoft Foundry project with:
|
||||
- A chat model deployment (the default is `gpt-5.4-mini`)
|
||||
- A `text-embedding-3-large` deployment with 3,072 dimensions
|
||||
3. An Azure Cosmos DB for NoSQL account with [vector search enabled](https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search)
|
||||
4. An Azure identity that can create the configured database and container and read and write items
|
||||
5. Azure CLI authentication (`az login`)
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | *(required)* |
|
||||
| `COSMOS_ENDPOINT` | Azure Cosmos DB account endpoint | *(required)* |
|
||||
| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` |
|
||||
| `FOUNDRY_EMBEDDING_MODEL` | Embedding model deployment name | `text-embedding-3-large` |
|
||||
| `FOUNDRY_EMBEDDING_DIMENSIONS` | Number of dimensions produced by the embedding deployment | `3072` |
|
||||
| `COSMOS_DATABASE_NAME` | Database used to store agent memory | `agent-memory` |
|
||||
|
||||
## Run the Sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The first session stores the user's preference for pirate jokes. The second session uses a different `AgentSession` but the same per-run user search scope, allowing the agent to retrieve that preference from Azure Cosmos DB without recalling data from earlier sample runs.
|
||||
@@ -10,8 +10,6 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|
||||
|[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.|
|
||||
|[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.|
|
||||
|[Memory Using AgentMemory](./AgentWithMemory_Step06_MemoryUsingAgentMemory/)|This sample demonstrates a retail shopping assistant built with [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), an unofficial .NET port of the Neo4j Labs graph-memory provider, to learn customer preferences and recommend products via graph traversal.|
|
||||
|[File Based Memory](./AgentWithMemory_Step07_FileMemoryProvider/)|This sample demonstrates how to use the `FileMemoryProvider` to give an agent tools for storing and recalling memories as files, and how to configure the folder that those memory files are written to.|
|
||||
|[Memory with Azure Cosmos DB for NoSQL](./AgentWithMemory_Step08_MemoryUsingCosmosNoSql/)|This sample demonstrates how to persist and retrieve chat history across sessions with Azure Cosmos DB for NoSQL.|
|
||||
|
||||
> **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
|
||||
|
||||
|
||||
@@ -81,12 +81,7 @@ public static class Program
|
||||
}
|
||||
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
|
||||
|
||||
// <rehydrate_workflow>
|
||||
// A rehydrated workflow must preserve the topology and executor identities of the workflow that
|
||||
// created the checkpoint. This executor-only workflow rebuilds identically because its executors
|
||||
// use fixed ids. Agent-based workflows must recreate each local agent with the same
|
||||
// ChatClientAgentOptions.Id (and, if set, the same Name), otherwise the executor ids no longer
|
||||
// match the checkpoint and resume fails.
|
||||
// Rehydrate a new workflow instance from a saved checkpoint and continue execution
|
||||
var newWorkflow = WorkflowFactory.BuildWorkflow();
|
||||
const int CheckpointIndex = 5;
|
||||
Console.WriteLine($"\n\nHydrating a new workflow instance from the {CheckpointIndex + 1}th checkpoint.");
|
||||
@@ -94,7 +89,6 @@ public static class Program
|
||||
|
||||
await using StreamingRun newCheckpointedRun =
|
||||
await InProcessExecution.ResumeStreamingAsync(newWorkflow, savedCheckpoint, checkpointManager);
|
||||
// </rehydrate_workflow>
|
||||
|
||||
await foreach (WorkflowEvent evt in newCheckpointedRun.WatchStreamAsync())
|
||||
{
|
||||
|
||||
@@ -10,81 +10,50 @@ using Microsoft.Extensions.AI;
|
||||
/// <param name="chatClient">The <see cref="IChatClient"/> to use as the agent backend.</param>
|
||||
internal sealed class AgentRegistry(IChatClient chatClient)
|
||||
{
|
||||
// <stable_agent_identity>
|
||||
// Give each agent a stable, unique Id so its workflow executor identity stays the same when the
|
||||
// workflow is reconstructed (for example per request or dependency-injection scope), which keeps
|
||||
// checkpoints resumable. If an agent also has a Name, keep that stable too, since the executor
|
||||
// identity includes it. Use a fixed logical role here, not a conversation, request, or user id.
|
||||
internal const string IntakeAgentName = "Assistant";
|
||||
public AIAgent IntakeAgent { get; } = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Id = "intake-agent",
|
||||
Name = IntakeAgentName,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
public AIAgent IntakeAgent { get; } = chatClient.AsAIAgent(
|
||||
instructions:
|
||||
"""
|
||||
You receive a user request and are responsible for routing to the correct initial expert agent.
|
||||
""",
|
||||
},
|
||||
});
|
||||
// </stable_agent_identity>
|
||||
IntakeAgentName
|
||||
);
|
||||
|
||||
internal const string LiquidityAnalysisAgentName = "Liquidity Analysis";
|
||||
public AIAgent LiquidityAnalysisAgent { get; } = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Id = "liquidity-analysis-agent",
|
||||
Name = LiquidityAnalysisAgentName,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
public AIAgent LiquidityAnalysisAgent { get; } = chatClient.AsAIAgent(
|
||||
instructions:
|
||||
"""
|
||||
You are responsible for Liquidity Analysis.
|
||||
""",
|
||||
},
|
||||
});
|
||||
LiquidityAnalysisAgentName
|
||||
);
|
||||
|
||||
internal const string TaxAnalysisAgentName = "Tax Analysis";
|
||||
public AIAgent TaxAnalysisAgent { get; } = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Id = "tax-analysis-agent",
|
||||
Name = TaxAnalysisAgentName,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are responsible for Tax Analysis.
|
||||
public AIAgent TaxAnalysisAgent { get; } = chatClient.AsAIAgent(
|
||||
instructions:
|
||||
"""
|
||||
You are responsible for Tax Analysis.
|
||||
""",
|
||||
},
|
||||
});
|
||||
TaxAnalysisAgentName
|
||||
);
|
||||
|
||||
internal const string ForeignExchangeAgentName = "Foreign Exchange Analysis";
|
||||
public AIAgent ForeignExchangeAgent { get; } = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Id = "foreign-exchange-agent",
|
||||
Name = ForeignExchangeAgentName,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are responsible for Foreign Exchange Analysis.
|
||||
public AIAgent ForeignExchangeAgent { get; } = chatClient.AsAIAgent(
|
||||
instructions:
|
||||
"""
|
||||
You are responsible for Foreign Exchange Analysis.
|
||||
""",
|
||||
},
|
||||
});
|
||||
ForeignExchangeAgentName
|
||||
);
|
||||
|
||||
internal const string EquityAgentName = "Equity Analysis";
|
||||
public AIAgent EquityAgent { get; } = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Id = "equity-analysis-agent",
|
||||
Name = EquityAgentName,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are responsible for Equity Analysis.
|
||||
public AIAgent EquityAgent { get; } = chatClient.AsAIAgent(
|
||||
instructions:
|
||||
"""
|
||||
You are responsible for Equity Analysis.
|
||||
""",
|
||||
},
|
||||
});
|
||||
EquityAgentName
|
||||
);
|
||||
|
||||
public IEnumerable<AIAgent> Experts => [this.LiquidityAnalysisAgent, this.TaxAnalysisAgent, this.ForeignExchangeAgent, this.EquityAgent];
|
||||
|
||||
|
||||
@@ -59,10 +59,6 @@ static async Task RunWorkflowAsync(Workflow workflow)
|
||||
}
|
||||
|
||||
await run.TrySendMessageAsync(userInput);
|
||||
|
||||
// Agents are wrapped as executors that cache incoming messages and only run when they receive a TurnToken,
|
||||
// so the turn must be triggered explicitly after sending the user input.
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
string? speakingAgent = null;
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cts.Token))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# .editorconfig
|
||||
[*.cs]
|
||||
|
||||
# See https://github.com/Azure/azure-functions-durable-extension/issues/3173
|
||||
dotnet_diagnostic.DURABLE0001.severity = none
|
||||
dotnet_diagnostic.DURABLE0002.severity = none
|
||||
dotnet_diagnostic.DURABLE0003.severity = none
|
||||
dotnet_diagnostic.DURABLE0004.severity = none
|
||||
dotnet_diagnostic.DURABLE0005.severity = none
|
||||
dotnet_diagnostic.DURABLE0006.severity = none
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<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>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</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>
|
||||
|
||||
<!-- 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,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
|
||||
// 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_NAME")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = client.GetChatClient(deploymentName).AsAIAgent(JokerInstructions, JokerName);
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
// This will automatically generate HTTP API endpoints for the agent.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -0,0 +1,89 @@
|
||||
# Single Agent Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
|
||||
- Registering agents with the Function app and running them using HTTP.
|
||||
- Conversation management (via session IDs) for isolated interactions.
|
||||
|
||||
## 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.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
|
||||
|
||||
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/agents/Joker/run `
|
||||
-ContentType text/plain `
|
||||
-Body "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
You can also send JSON requests:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me a joke about a pirate."}'
|
||||
```
|
||||
|
||||
To continue a conversation, include the `thread_id` in the query string or JSON body:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me another one."}'
|
||||
```
|
||||
|
||||
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
|
||||
|
||||
```text
|
||||
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
|
||||
```
|
||||
|
||||
The expected `application/json` output will look something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
|
||||
"response": {
|
||||
"Messages": [
|
||||
{
|
||||
"AuthorName": "Joker",
|
||||
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
|
||||
"Role": "assistant",
|
||||
"Contents": [
|
||||
{
|
||||
"Type": "text",
|
||||
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Usage": {
|
||||
"InputTokenCount": 78,
|
||||
"OutputTokenCount": 36,
|
||||
"TotalTokenCount": 114
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Prompt the agent
|
||||
POST {{authority}}/api/agents/Joker/run
|
||||
Content-Type: text/plain
|
||||
|
||||
Tell me a joke about a pirate.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<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>AgentOrchestration_Chaining</AssemblyName>
|
||||
<RootNamespace>AgentOrchestration_Chaining</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>
|
||||
|
||||
<!-- 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>
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
|
||||
namespace AgentOrchestration_Chaining;
|
||||
|
||||
public static class FunctionTriggers
|
||||
{
|
||||
public sealed record TextResponse(string Text);
|
||||
|
||||
[Function(nameof(RunOrchestrationAsync))]
|
||||
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
DurableAIAgent writer = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writer.CreateSessionAsync();
|
||||
|
||||
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
|
||||
message: "Write a concise inspirational sentence about learning.",
|
||||
session: writerSession);
|
||||
|
||||
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
|
||||
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
|
||||
session: writerSession);
|
||||
|
||||
return refined.Result.Text;
|
||||
}
|
||||
|
||||
// POST /singleagent/run
|
||||
[Function(nameof(StartOrchestrationAsync))]
|
||||
public static async Task<HttpResponseData> StartOrchestrationAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "singleagent/run")] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
|
||||
orchestratorName: nameof(RunOrchestrationAsync));
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "Single-agent orchestration started.",
|
||||
instanceId,
|
||||
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// GET /singleagent/status/{instanceId}
|
||||
[Function(nameof(GetOrchestrationStatusAsync))]
|
||||
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "singleagent/status/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
OrchestrationMetadata? status = await client.GetInstanceAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
req.FunctionContext.CancellationToken);
|
||||
|
||||
if (status is null)
|
||||
{
|
||||
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
|
||||
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
|
||||
return notFound;
|
||||
}
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
instanceId = status.InstanceId,
|
||||
runtimeStatus = status.RuntimeStatus.ToString(),
|
||||
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
|
||||
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
|
||||
failureDetails = status.FailureDetails
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
|
||||
{
|
||||
// NOTE: This can be made more robust by considering the value of
|
||||
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
|
||||
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
|
||||
return $"{authority}/api/singleagent/status/{instanceId}";
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
|
||||
// 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_NAME")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
|
||||
const string WriterName = "WriterAgent";
|
||||
const string WriterInstructions =
|
||||
"""
|
||||
You refine short pieces of text. When given an initial sentence you enhance it;
|
||||
when given an improved sentence you polish it further.
|
||||
""";
|
||||
|
||||
AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstructions, WriterName);
|
||||
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
|
||||
.Build();
|
||||
|
||||
app.Run();
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Single Agent Orchestration Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Orchestrating multiple interactions with the same agent in a deterministic order
|
||||
- Using the same `AgentSession` across multiple calls to maintain conversational context
|
||||
- Durable orchestration with automatic checkpointing and resumption from failures
|
||||
- HTTP API integration for starting and monitoring orchestrations
|
||||
|
||||
## 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.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request to start the orchestration.
|
||||
|
||||
You can use the `demo.http` file to start the orchestration, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/singleagent/run
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post -Uri http://localhost:7071/api/singleagent/run
|
||||
```
|
||||
|
||||
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Single-agent orchestration started.",
|
||||
"instanceId": "86313f1d45fb42eeb50b1852626bf3ff",
|
||||
"statusQueryGetUri": "http://localhost:7071/api/singleagent/status/86313f1d45fb42eeb50b1852626bf3ff"
|
||||
}
|
||||
```
|
||||
|
||||
The orchestration will proceed to run the WriterAgent twice in sequence:
|
||||
|
||||
1. First, it writes an inspirational sentence about learning
|
||||
2. Then, it refines the initial output using the same conversation thread
|
||||
|
||||
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"failureDetails": null,
|
||||
"input": null,
|
||||
"instanceId": "86313f1d45fb42eeb50b1852626bf3ff",
|
||||
"output": "Learning serves as the key, opening doors to boundless opportunities and a brighter future.",
|
||||
"runtimeStatus": "Completed"
|
||||
}
|
||||
```
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
### Start the single-agent orchestration
|
||||
POST http://localhost:7071/api/singleagent/run
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<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>AgentOrchestration_Concurrency</AssemblyName>
|
||||
<RootNamespace>AgentOrchestration_Concurrency</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>
|
||||
|
||||
<!-- 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>
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
|
||||
namespace AgentOrchestration_Concurrency;
|
||||
|
||||
public static class FunctionsTriggers
|
||||
{
|
||||
public sealed record TextResponse(string Text);
|
||||
|
||||
[Function(nameof(RunOrchestrationAsync))]
|
||||
public static async Task<object> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
// Get the prompt from the orchestration input
|
||||
string prompt = context.GetInput<string>() ?? throw new InvalidOperationException("Prompt is required");
|
||||
|
||||
// Get both agents
|
||||
DurableAIAgent physicist = context.GetAgent("PhysicistAgent");
|
||||
DurableAIAgent chemist = context.GetAgent("ChemistAgent");
|
||||
|
||||
// Start both agent runs concurrently
|
||||
Task<AgentResponse<TextResponse>> physicistTask = physicist.RunAsync<TextResponse>(prompt);
|
||||
|
||||
Task<AgentResponse<TextResponse>> chemistTask = chemist.RunAsync<TextResponse>(prompt);
|
||||
|
||||
// Wait for both tasks to complete using Task.WhenAll
|
||||
await Task.WhenAll(physicistTask, chemistTask);
|
||||
|
||||
// Get the results
|
||||
TextResponse physicistResponse = (await physicistTask).Result;
|
||||
TextResponse chemistResponse = (await chemistTask).Result;
|
||||
|
||||
// Return the result as a structured, anonymous type
|
||||
return new
|
||||
{
|
||||
physicist = physicistResponse.Text,
|
||||
chemist = chemistResponse.Text,
|
||||
};
|
||||
}
|
||||
|
||||
// POST /multiagent/run
|
||||
[Function(nameof(StartOrchestrationAsync))]
|
||||
public static async Task<HttpResponseData> StartOrchestrationAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "multiagent/run")] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
// Read the prompt from the request body
|
||||
string? prompt = await req.ReadAsStringAsync();
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequestResponse.WriteAsJsonAsync(new { error = "Prompt is required" });
|
||||
return badRequestResponse;
|
||||
}
|
||||
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
|
||||
orchestratorName: nameof(RunOrchestrationAsync),
|
||||
input: prompt);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "Multi-agent concurrent orchestration started.",
|
||||
prompt,
|
||||
instanceId,
|
||||
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// GET /multiagent/status/{instanceId}
|
||||
[Function(nameof(GetOrchestrationStatusAsync))]
|
||||
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "multiagent/status/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
OrchestrationMetadata? status = await client.GetInstanceAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
req.FunctionContext.CancellationToken);
|
||||
|
||||
if (status is null)
|
||||
{
|
||||
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
|
||||
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
|
||||
return notFound;
|
||||
}
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
instanceId = status.InstanceId,
|
||||
runtimeStatus = status.RuntimeStatus.ToString(),
|
||||
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
|
||||
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
|
||||
failureDetails = status.FailureDetails
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
|
||||
{
|
||||
// NOTE: This can be made more robust by considering the value of
|
||||
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
|
||||
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
|
||||
return $"{authority}/api/multiagent/status/{instanceId}";
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
|
||||
// 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_NAME")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Two agents used by the orchestration to demonstrate concurrent execution.
|
||||
const string PhysicistName = "PhysicistAgent";
|
||||
const string PhysicistInstructions = "You are an expert in physics. You answer questions from a physics perspective.";
|
||||
|
||||
const string ChemistName = "ChemistAgent";
|
||||
const string ChemistInstructions = "You are an expert in chemistry. You answer questions from a chemistry perspective.";
|
||||
|
||||
AIAgent physicistAgent = client.GetChatClient(deploymentName).AsAIAgent(PhysicistInstructions, PhysicistName);
|
||||
AIAgent chemistAgent = client.GetChatClient(deploymentName).AsAIAgent(ChemistInstructions, ChemistName);
|
||||
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options =>
|
||||
{
|
||||
options
|
||||
.AddAIAgent(physicistAgent)
|
||||
.AddAIAgent(chemistAgent);
|
||||
})
|
||||
.Build();
|
||||
|
||||
app.Run();
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# Multi-Agent Concurrent Orchestration Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create an Azure Functions app that orchestrates concurrent execution of multiple AI agents, each with specialized expertise, to provide comprehensive answers to complex questions.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Multi-agent orchestration with specialized AI agents (physics and chemistry)
|
||||
- Concurrent execution using the fan-out/fan-in pattern for improved performance and distributed processing
|
||||
- Response aggregation from multiple agents into a unified result
|
||||
- Durable orchestration with automatic checkpointing and resumption from failures
|
||||
|
||||
## 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.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request with a custom prompt to the orchestration.
|
||||
|
||||
You can use the `demo.http` file to send a message to the agents, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/multiagent/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "What is temperature?"
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/multiagent/run `
|
||||
-ContentType text/plain `
|
||||
-Body "What is temperature?"
|
||||
```
|
||||
|
||||
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Multi-agent concurrent orchestration started.",
|
||||
"prompt": "What is temperature?",
|
||||
"instanceId": "e7e29999b6b8424682b3539292afc9ed",
|
||||
"statusQueryGetUri": "http://localhost:7071/api/multiagent/status/e7e29999b6b8424682b3539292afc9ed"
|
||||
}
|
||||
```
|
||||
|
||||
The orchestration will run both the PhysicistAgent and ChemistAgent concurrently, asking them the same question. Their responses will be combined to provide a comprehensive answer covering both physical and chemical aspects.
|
||||
|
||||
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"failureDetails": null,
|
||||
"input": "What is temperature?",
|
||||
"instanceId": "e7e29999b6b8424682b3539292afc9ed",
|
||||
"output": {
|
||||
"physicist": "Temperature is a measure of the average kinetic energy of particles in a system. From a physics perspective, it represents the thermal energy and determines the direction of heat flow between objects.",
|
||||
"chemist": "From a chemistry perspective, temperature is crucial for chemical reactions as it affects reaction rates through the Arrhenius equation. It influences the equilibrium position of reversible reactions and determines the physical state of substances."
|
||||
},
|
||||
"runtimeStatus": "Completed"
|
||||
}
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
### Start the multi-agent concurrent orchestration
|
||||
POST http://localhost:7071/api/multiagent/run
|
||||
Content-Type: text/plain
|
||||
|
||||
What is temperature?
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<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>AgentOrchestration_Conditionals</AssemblyName>
|
||||
<RootNamespace>AgentOrchestration_Conditionals</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>
|
||||
|
||||
<!-- 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>
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
|
||||
namespace AgentOrchestration_Conditionals;
|
||||
|
||||
public static class FunctionTriggers
|
||||
{
|
||||
[Function(nameof(RunOrchestrationAsync))]
|
||||
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
// Get the email from the orchestration input
|
||||
Email email = context.GetInput<Email>() ?? throw new InvalidOperationException("Email is required");
|
||||
|
||||
// Get the spam detection agent
|
||||
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
|
||||
AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync();
|
||||
|
||||
// Step 1: Check if the email is spam
|
||||
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
|
||||
message:
|
||||
$"""
|
||||
Analyze this email for spam content and return a JSON response with 'is_spam' (boolean) and 'reason' (string) fields:
|
||||
Email ID: {email.EmailId}
|
||||
Content: {email.EmailContent}
|
||||
""",
|
||||
session: spamSession);
|
||||
DetectionResult result = spamDetectionResponse.Result;
|
||||
|
||||
// Step 2: Conditional logic based on spam detection result
|
||||
if (result.IsSpam)
|
||||
{
|
||||
// Handle spam email
|
||||
return await context.CallActivityAsync<string>(nameof(HandleSpamEmail), result.Reason);
|
||||
}
|
||||
|
||||
// Generate and send response for legitimate email
|
||||
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
|
||||
AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync();
|
||||
|
||||
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
|
||||
message:
|
||||
$"""
|
||||
Draft a professional response to this email. Return a JSON response with a 'response' field containing the reply:
|
||||
|
||||
Email ID: {email.EmailId}
|
||||
Content: {email.EmailContent}
|
||||
""",
|
||||
session: emailSession);
|
||||
|
||||
EmailResponse emailResponse = emailAssistantResponse.Result;
|
||||
|
||||
return await context.CallActivityAsync<string>(nameof(SendEmail), emailResponse.Response);
|
||||
}
|
||||
|
||||
[Function(nameof(HandleSpamEmail))]
|
||||
public static string HandleSpamEmail([ActivityTrigger] string reason)
|
||||
{
|
||||
return $"Email marked as spam: {reason}";
|
||||
}
|
||||
|
||||
[Function(nameof(SendEmail))]
|
||||
public static string SendEmail([ActivityTrigger] string message)
|
||||
{
|
||||
return $"Email sent: {message}";
|
||||
}
|
||||
|
||||
// POST /spamdetection/run
|
||||
[Function(nameof(StartOrchestrationAsync))]
|
||||
public static async Task<HttpResponseData> StartOrchestrationAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "spamdetection/run")] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
// Read the email from the request body
|
||||
Email? email = await req.ReadFromJsonAsync<Email>();
|
||||
if (email is null || string.IsNullOrWhiteSpace(email.EmailContent))
|
||||
{
|
||||
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequestResponse.WriteAsJsonAsync(new { error = "Email with content is required" });
|
||||
return badRequestResponse;
|
||||
}
|
||||
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
|
||||
orchestratorName: nameof(RunOrchestrationAsync),
|
||||
input: email);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "Spam detection orchestration started.",
|
||||
emailId = email.EmailId,
|
||||
instanceId,
|
||||
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// GET /spamdetection/status/{instanceId}
|
||||
[Function(nameof(GetOrchestrationStatusAsync))]
|
||||
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "spamdetection/status/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
OrchestrationMetadata? status = await client.GetInstanceAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
req.FunctionContext.CancellationToken);
|
||||
|
||||
if (status is null)
|
||||
{
|
||||
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
|
||||
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
|
||||
return notFound;
|
||||
}
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
instanceId = status.InstanceId,
|
||||
runtimeStatus = status.RuntimeStatus.ToString(),
|
||||
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
|
||||
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
|
||||
failureDetails = status.FailureDetails
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
|
||||
{
|
||||
// NOTE: This can be made more robust by considering the value of
|
||||
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
|
||||
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
|
||||
return $"{authority}/api/spamdetection/status/{instanceId}";
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AgentOrchestration_Conditionals;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an email input for spam detection and response generation.
|
||||
/// </summary>
|
||||
public sealed class Email
|
||||
{
|
||||
[JsonPropertyName("email_id")]
|
||||
public string EmailId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("email_content")]
|
||||
public string EmailContent { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of spam detection analysis.
|
||||
/// </summary>
|
||||
public sealed class DetectionResult
|
||||
{
|
||||
[JsonPropertyName("is_spam")]
|
||||
public bool IsSpam { get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a generated email response.
|
||||
/// </summary>
|
||||
public sealed class EmailResponse
|
||||
{
|
||||
[JsonPropertyName("response")]
|
||||
public string Response { get; set; } = string.Empty;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
|
||||
// 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_NAME")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Two agents used by the orchestration to demonstrate conditional logic.
|
||||
const string SpamDetectionName = "SpamDetectionAgent";
|
||||
const string SpamDetectionInstructions = "You are a spam detection assistant that identifies spam emails.";
|
||||
|
||||
const string EmailAssistantName = "EmailAssistantAgent";
|
||||
const string EmailAssistantInstructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
|
||||
|
||||
AIAgent spamDetectionAgent = client.GetChatClient(deploymentName)
|
||||
.AsAIAgent(SpamDetectionInstructions, SpamDetectionName);
|
||||
|
||||
AIAgent emailAssistantAgent = client.GetChatClient(deploymentName)
|
||||
.AsAIAgent(EmailAssistantInstructions, EmailAssistantName);
|
||||
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options =>
|
||||
{
|
||||
options
|
||||
.AddAIAgent(spamDetectionAgent)
|
||||
.AddAIAgent(emailAssistantAgent);
|
||||
})
|
||||
.Build();
|
||||
|
||||
app.Run();
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Multi-Agent Orchestration with Conditionals Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a multi-agent orchestration workflow that includes conditional logic. The workflow implements a spam detection system that processes emails and takes different actions based on whether the email is identified as spam or legitimate.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Multi-agent orchestration with conditional logic and different processing paths
|
||||
- Spam detection using AI agent analysis
|
||||
- Structured output from agents for reliable processing
|
||||
- Activity functions for integrating non-agentic workflow actions
|
||||
|
||||
## 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.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request with email data to the orchestration.
|
||||
|
||||
You can use the `demo.http` file to send email data to the agents, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
# Test with a legitimate email
|
||||
curl -X POST http://localhost:7071/api/spamdetection/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email_id": "email-001",
|
||||
"email_content": "Hi John, I hope you are doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
|
||||
}'
|
||||
|
||||
# Test with a spam email
|
||||
curl -X POST http://localhost:7071/api/spamdetection/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email_id": "email-002",
|
||||
"email_content": "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!"
|
||||
}'
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
# Test with a legitimate email
|
||||
$body = @{
|
||||
email_id = "email-001"
|
||||
email_content = "Hi John, I hope you are doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/spamdetection/run `
|
||||
-ContentType application/json `
|
||||
-Body $body
|
||||
|
||||
# Test with a spam email
|
||||
$body = @{
|
||||
email_id = "email-002"
|
||||
email_content = "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!"
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/spamdetection/run `
|
||||
-ContentType application/json `
|
||||
-Body $body
|
||||
```
|
||||
|
||||
The response from either input will be a JSON object that looks something like the following, which indicates that the orchestration has started.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Spam detection orchestration started.",
|
||||
"emailId": "email-001",
|
||||
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
|
||||
"statusQueryGetUri": "http://localhost:7071/api/spamdetection/status/555dbbb63f75406db2edf9f1f092de95"
|
||||
}
|
||||
```
|
||||
|
||||
The orchestration will:
|
||||
|
||||
1. Analyze the email content using the SpamDetectionAgent
|
||||
2. If spam: Mark the email as spam with a reason
|
||||
3. If legitimate: Use the EmailAssistantAgent to draft a professional response and "send" it
|
||||
|
||||
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response for the legitimate email will be a JSON object that looks something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"failureDetails": null,
|
||||
"input": {
|
||||
"email_content": "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!",
|
||||
"email_id": "email-001"
|
||||
},
|
||||
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
|
||||
"output": "Email sent: Subject: Re: Follow-Up on Quarterly Report\n\nHi [Recipient's Name],\n\nI hope this message finds you well. Thank you for your patience. I will ensure the updated figures for the quarterly report are sent to you by Friday.\n\nIf you have any further questions or need additional information, please feel free to reach out.\n\nBest regards,\n\nJohn",
|
||||
"runtimeStatus": "Completed"
|
||||
}
|
||||
```
|
||||
|
||||
The response for the spam email will be a JSON object that looks something like the following, which indicates that the email was marked as spam:
|
||||
|
||||
```json
|
||||
{
|
||||
"failureDetails": null,
|
||||
"input": {
|
||||
"email_content": "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!",
|
||||
"email_id": "email-002"
|
||||
},
|
||||
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
|
||||
"output": "Email marked as spam: The email contains misleading claims of winning a large sum of money and encourages immediate action, which are common characteristics of spam.",
|
||||
"runtimeStatus": "Completed"
|
||||
}
|
||||
```
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
### Test spam detection with a legitimate email
|
||||
POST http://localhost:7071/api/spamdetection/run
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email_id": "email-001",
|
||||
"email_content": "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
|
||||
}
|
||||
|
||||
|
||||
### Test spam detection with a spam email
|
||||
POST http://localhost:7071/api/spamdetection/run
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email_id": "email-002",
|
||||
"email_content": "URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<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>AgentOrchestration_HITL</AssemblyName>
|
||||
<RootNamespace>AgentOrchestration_HITL</RootNamespace>
|
||||
<NoWarn>$(NoWarn);DURABLE0001;DURABLE0002;DURABLE0003;DURABLE0004;DURABLE0005;DURABLE0006</NoWarn>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AgentOrchestration_HITL;
|
||||
|
||||
public static class FunctionTriggers
|
||||
{
|
||||
[Function(nameof(RunOrchestrationAsync))]
|
||||
public static async Task<object> RunOrchestrationAsync(
|
||||
[OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
// Get the input from the orchestration
|
||||
ContentGenerationInput input = context.GetInput<ContentGenerationInput>()
|
||||
?? throw new InvalidOperationException("Content generation input is required");
|
||||
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writerAgent.CreateSessionAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
|
||||
// Step 1: Generate initial content
|
||||
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
|
||||
message: $"Write a short article about '{input.Topic}'.",
|
||||
session: writerSession);
|
||||
GeneratedContent content = writerResponse.Result;
|
||||
|
||||
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
|
||||
int iterationCount = 0;
|
||||
while (iterationCount++ < input.MaxReviewAttempts)
|
||||
{
|
||||
context.SetCustomStatus(
|
||||
$"Requesting human feedback. Iteration #{iterationCount}. Timeout: {input.ApprovalTimeoutHours} hour(s).");
|
||||
|
||||
// Step 2: Notify user to review the content
|
||||
await context.CallActivityAsync(nameof(NotifyUserForApproval), content);
|
||||
|
||||
// Step 3: Wait for human feedback with configurable timeout
|
||||
HumanApprovalResponse humanResponse;
|
||||
try
|
||||
{
|
||||
humanResponse = await context.WaitForExternalEvent<HumanApprovalResponse>(
|
||||
eventName: "HumanApproval",
|
||||
timeout: TimeSpan.FromHours(input.ApprovalTimeoutHours));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Timeout occurred - treat as rejection
|
||||
context.SetCustomStatus(
|
||||
$"Human approval timed out after {input.ApprovalTimeoutHours} hour(s). Treating as rejection.");
|
||||
throw new TimeoutException($"Human approval timed out after {input.ApprovalTimeoutHours} hour(s).");
|
||||
}
|
||||
|
||||
if (humanResponse.Approved)
|
||||
{
|
||||
context.SetCustomStatus("Content approved by human reviewer. Publishing content...");
|
||||
|
||||
// Step 4: Publish the approved content
|
||||
await context.CallActivityAsync(nameof(PublishContent), content);
|
||||
|
||||
context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime:s}");
|
||||
return new { content = content.Content };
|
||||
}
|
||||
|
||||
context.SetCustomStatus("Content rejected by human reviewer. Incorporating feedback and regenerating...");
|
||||
|
||||
// Incorporate human feedback and regenerate
|
||||
writerResponse = await writerAgent.RunAsync<GeneratedContent>(
|
||||
message: $"""
|
||||
The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.
|
||||
|
||||
Human Feedback: {humanResponse.Feedback}
|
||||
""",
|
||||
session: writerSession);
|
||||
|
||||
content = writerResponse.Result;
|
||||
}
|
||||
|
||||
// If we reach here, it means we exhausted the maximum number of iterations
|
||||
throw new InvalidOperationException(
|
||||
$"Content could not be approved after {input.MaxReviewAttempts} iterations.");
|
||||
}
|
||||
|
||||
// POST /hitl/run
|
||||
[Function(nameof(StartOrchestrationAsync))]
|
||||
public static async Task<HttpResponseData> StartOrchestrationAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/run")] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
// Read the input from the request body
|
||||
ContentGenerationInput? input = await req.ReadFromJsonAsync<ContentGenerationInput>();
|
||||
if (input is null || string.IsNullOrWhiteSpace(input.Topic))
|
||||
{
|
||||
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequestResponse.WriteAsJsonAsync(new { error = "Topic is required" });
|
||||
return badRequestResponse;
|
||||
}
|
||||
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
|
||||
orchestratorName: nameof(RunOrchestrationAsync),
|
||||
input: input);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "HITL content generation orchestration started.",
|
||||
topic = input.Topic,
|
||||
instanceId,
|
||||
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// POST /hitl/approve/{instanceId}
|
||||
[Function(nameof(SendHumanApprovalAsync))]
|
||||
public static async Task<HttpResponseData> SendHumanApprovalAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/approve/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
// Read the approval response from the request body
|
||||
HumanApprovalResponse? approvalResponse = await req.ReadFromJsonAsync<HumanApprovalResponse>();
|
||||
if (approvalResponse is null)
|
||||
{
|
||||
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequestResponse.WriteAsJsonAsync(new { error = "Approval response is required" });
|
||||
return badRequestResponse;
|
||||
}
|
||||
|
||||
// Send the approval event to the orchestration
|
||||
await client.RaiseEventAsync(instanceId, "HumanApproval", approvalResponse);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "Human approval sent to orchestration.",
|
||||
instanceId,
|
||||
approved = approvalResponse.Approved
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// GET /hitl/status/{instanceId}
|
||||
[Function(nameof(GetOrchestrationStatusAsync))]
|
||||
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "hitl/status/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
OrchestrationMetadata? status = await client.GetInstanceAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
req.FunctionContext.CancellationToken);
|
||||
|
||||
if (status is null)
|
||||
{
|
||||
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
|
||||
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
|
||||
return notFound;
|
||||
}
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
instanceId = status.InstanceId,
|
||||
runtimeStatus = status.RuntimeStatus.ToString(),
|
||||
workflowStatus = status.SerializedCustomStatus is not null ? (object)status.ReadCustomStatusAs<JsonElement>() : null,
|
||||
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
|
||||
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
|
||||
failureDetails = status.FailureDetails
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
[Function(nameof(NotifyUserForApproval))]
|
||||
public static void NotifyUserForApproval(
|
||||
[ActivityTrigger] GeneratedContent content,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
ILogger logger = functionContext.GetLogger(nameof(NotifyUserForApproval));
|
||||
|
||||
// In a real implementation, this would send notifications via email, SMS, etc.
|
||||
logger.LogInformation(
|
||||
"""
|
||||
NOTIFICATION: Please review the following content for approval:
|
||||
Title: {Title}
|
||||
Content: {Content}
|
||||
Use the approval endpoint to approve or reject this content.
|
||||
""",
|
||||
content.Title,
|
||||
content.Content);
|
||||
}
|
||||
|
||||
[Function(nameof(PublishContent))]
|
||||
public static void PublishContent(
|
||||
[ActivityTrigger] GeneratedContent content,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
ILogger logger = functionContext.GetLogger(nameof(PublishContent));
|
||||
|
||||
// In a real implementation, this would publish to a CMS, website, etc.
|
||||
logger.LogInformation(
|
||||
"""
|
||||
PUBLISHING: Content has been published successfully.
|
||||
Title: {Title}
|
||||
Content: {Content}
|
||||
""",
|
||||
content.Title,
|
||||
content.Content);
|
||||
}
|
||||
|
||||
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
|
||||
{
|
||||
// NOTE: This can be made more robust by considering the value of
|
||||
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
|
||||
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
|
||||
return $"{authority}/api/hitl/status/{instanceId}";
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AgentOrchestration_HITL;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the input for the Human-in-the-Loop content generation workflow.
|
||||
/// </summary>
|
||||
public sealed class ContentGenerationInput
|
||||
{
|
||||
[JsonPropertyName("topic")]
|
||||
public string Topic { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("max_review_attempts")]
|
||||
public int MaxReviewAttempts { get; set; } = 3;
|
||||
|
||||
[JsonPropertyName("approval_timeout_hours")]
|
||||
public float ApprovalTimeoutHours { get; set; } = 72;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the content generated by the writer agent.
|
||||
/// </summary>
|
||||
public sealed class GeneratedContent
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the human approval response.
|
||||
/// </summary>
|
||||
public sealed class HumanApprovalResponse
|
||||
{
|
||||
[JsonPropertyName("approved")]
|
||||
public bool Approved { get; set; }
|
||||
|
||||
[JsonPropertyName("feedback")]
|
||||
public string Feedback { get; set; } = string.Empty;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0002 // Simplify Member Access
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
|
||||
// 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_NAME")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate human-in-the-loop workflow.
|
||||
const string WriterName = "WriterAgent";
|
||||
const string WriterInstructions =
|
||||
"""
|
||||
You are a professional content writer who creates high-quality articles on various topics.
|
||||
You write engaging, informative, and well-structured content that follows best practices for readability and accuracy.
|
||||
""";
|
||||
|
||||
AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstructions, WriterName);
|
||||
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
|
||||
.Build();
|
||||
|
||||
app.Run();
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# Multi-Agent Orchestration with Human-in-the-Loop Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a human-in-the-loop (HITL) workflow using a single AI agent. The workflow uses a writer agent to generate content and requires human approval on every iteration, emphasizing the human-in-the-loop pattern.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Single-agent orchestration
|
||||
- Human-in-the-loop feedback loop using external events (`WaitForExternalEvent`)
|
||||
- Activity functions for non-agentic workflow steps
|
||||
- Iterative content refinement based on human feedback
|
||||
- Custom status tracking for workflow visibility
|
||||
- Error handling with maximum retry attempts and timeout handling for human approval
|
||||
|
||||
## 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.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request with a topic to start the content generation workflow.
|
||||
|
||||
You can use the `demo.http` file to send a topic to the agents, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/hitl/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"max_review_attempts": 3,
|
||||
"timeout_minutes": 5
|
||||
}'
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
topic = "The Future of Artificial Intelligence"
|
||||
max_review_attempts = 3
|
||||
timeout_minutes = 5
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/hitl/run `
|
||||
-ContentType application/json `
|
||||
-Body $body
|
||||
```
|
||||
|
||||
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "HITL content generation orchestration started.",
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"statusQueryGetUri": "http://localhost:7071/api/hitl/status/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
|
||||
}
|
||||
```
|
||||
|
||||
The orchestration will:
|
||||
|
||||
1. Generate initial content using the WriterAgent
|
||||
2. Notify the user to review the content
|
||||
3. Wait for human feedback via external event (configurable timeout)
|
||||
4. If approved by human, publish the content
|
||||
5. If rejected by human, incorporate feedback and regenerate content
|
||||
6. If approval timeout occurs, treat as rejection and fail the orchestration
|
||||
7. Repeat until human approval is received or maximum loop iterations are reached
|
||||
|
||||
Once the orchestration is waiting for human approval, you can send approval or rejection using the approval endpoint:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
# Approve the content
|
||||
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"approved": true,
|
||||
"feedback": "Great article! The content is well-structured and informative."
|
||||
}'
|
||||
|
||||
# Reject the content with feedback
|
||||
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"approved": false,
|
||||
"feedback": "The article needs more technical depth and better examples."
|
||||
}'
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
# Approve the content
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
|
||||
-ContentType application/json `
|
||||
-Body '{ "approved": true, "feedback": "Great article! The content is well-structured and informative." }'
|
||||
|
||||
# Reject the content with feedback
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
|
||||
-ContentType application/json `
|
||||
-Body '{ "approved": false, "feedback": "The article needs more technical depth and better examples." }'
|
||||
```
|
||||
|
||||
Once the orchestration has completed, you can get the status by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"failureDetails": null,
|
||||
"input": {
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"max_review_attempts": 3
|
||||
},
|
||||
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"output": {
|
||||
"content": "The Future of Artificial Intelligence is..."
|
||||
},
|
||||
"runtimeStatus": "Completed",
|
||||
"workflowStatus": "Content published successfully at 2025-10-15T12:00:00Z"
|
||||
}
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user