mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4b732741 |
@@ -1,31 +1,16 @@
|
||||
{
|
||||
"name": "C# (.NET)",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/powershell:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/azure/azure-dev/azd:0": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
||||
"version": "none",
|
||||
"dotnetRuntimeVersions": "10.0",
|
||||
"aspNetCoreRuntimeVersions": "10.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/copilot-cli:1": {}
|
||||
"ghcr.io/devcontainers/features/dotnet:2.4.0": {},
|
||||
"ghcr.io/devcontainers/features/powershell:1.5.1": {},
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2.12.4": {}
|
||||
},
|
||||
"workspaceFolder": "/workspaces/agent-framework/dotnet/",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"GitHub.copilot",
|
||||
"GitHub.vscode-github-actions",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"ms-windows-ai-studio.windows-ai-studio"
|
||||
|
||||
@@ -20,8 +20,6 @@ ignorePatterns:
|
||||
- pattern: "https://your-resource.openai.azure.com/"
|
||||
- pattern: "http://host.docker.internal"
|
||||
- pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/"
|
||||
- pattern: "https:\/\/dotnet.microsoft.com\/download"
|
||||
- pattern: "https://github.com/Rel1cx/eslint-react"
|
||||
# excludedDirs:
|
||||
# Folders which include links to localhost, since it's not ignored with regular expressions
|
||||
baseUrl: https://github.com/microsoft/agent-framework/
|
||||
|
||||
@@ -2,6 +2,3 @@
|
||||
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
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
|
||||
|
||||
@@ -47,7 +47,7 @@ body:
|
||||
attributes:
|
||||
label: Package Versions
|
||||
description: List the agent-framework-* packages and versions you are using
|
||||
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-foundry: 1.0.0"
|
||||
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ inputs:
|
||||
os:
|
||||
description: The operating system to set up
|
||||
required: true
|
||||
exclude-packages:
|
||||
description: Space-separated list of packages to exclude from uv sync
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -23,20 +19,6 @@ runs:
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ inputs.os }}-${{ inputs.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Exclude incompatible workspace packages
|
||||
if: ${{ inputs.exclude-packages != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
for pkg in ${{ inputs.exclude-packages }}; do
|
||||
for f in python/packages/*/pyproject.toml; do
|
||||
if grep -q "name = \"$pkg\"" "$f"; then
|
||||
pkg_dir=$(dirname "$f" | sed 's|python/||')
|
||||
echo "Excluding workspace package: $pkg ($pkg_dir)"
|
||||
sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml
|
||||
sed -i.bak '/'"$pkg"' = { workspace = true }/d' python/pyproject.toml
|
||||
fi
|
||||
done
|
||||
done
|
||||
- name: Install the project
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Sample Validation Setup
|
||||
description: Sets up the environment for sample validation (checkout, Node.js, Copilot CLI, Azure login, Python)
|
||||
|
||||
inputs:
|
||||
azure-client-id:
|
||||
description: Azure Client ID for OIDC login
|
||||
required: true
|
||||
azure-tenant-id:
|
||||
description: Azure Tenant ID for OIDC login
|
||||
required: true
|
||||
azure-subscription-id:
|
||||
description: Azure Subscription ID for OIDC login
|
||||
required: true
|
||||
python-version:
|
||||
description: The Python version to set up
|
||||
required: false
|
||||
default: "3.12"
|
||||
os:
|
||||
description: The operating system to set up
|
||||
required: false
|
||||
default: "Linux"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Node.js environment
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install Copilot CLI
|
||||
shell: bash
|
||||
run: npm install -g @github/copilot
|
||||
|
||||
- name: Test Copilot CLI
|
||||
shell: bash
|
||||
run: copilot --version && copilot -p "What can you do in one sentence?"
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@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 }}
|
||||
@@ -1,166 +0,0 @@
|
||||
name: Setup Local MCP Server
|
||||
description: Start and validate a local streamable HTTP MCP server for integration tests
|
||||
|
||||
inputs:
|
||||
fallback_url:
|
||||
description: Existing LOCAL_MCP_URL value to keep as a fallback if local startup fails
|
||||
required: false
|
||||
default: ''
|
||||
host:
|
||||
description: Host interface to bind the local MCP server
|
||||
required: false
|
||||
default: '127.0.0.1'
|
||||
port:
|
||||
description: Port to bind the local MCP server
|
||||
required: false
|
||||
default: '8011'
|
||||
mount_path:
|
||||
description: Mount path for the local streamable HTTP MCP endpoint
|
||||
required: false
|
||||
default: '/mcp'
|
||||
|
||||
outputs:
|
||||
effective_url:
|
||||
description: Local MCP URL when startup succeeds, otherwise the provided fallback URL
|
||||
value: ${{ steps.start.outputs.effective_url }}
|
||||
local_url:
|
||||
description: URL of the local MCP server
|
||||
value: ${{ steps.start.outputs.local_url }}
|
||||
started:
|
||||
description: Whether the local MCP server started and passed validation
|
||||
value: ${{ steps.start.outputs.started }}
|
||||
pid:
|
||||
description: PID of the local MCP server process when startup succeeded
|
||||
value: ${{ steps.start.outputs.pid }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Start and validate local MCP server
|
||||
id: start
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
host="${{ inputs.host }}"
|
||||
port="${{ inputs.port }}"
|
||||
mount_path="${{ inputs.mount_path }}"
|
||||
fallback_url="${{ inputs.fallback_url }}"
|
||||
|
||||
if [[ ! "$mount_path" =~ ^/ ]]; then
|
||||
mount_path="/$mount_path"
|
||||
fi
|
||||
|
||||
local_url="http://${host}:${port}${mount_path}"
|
||||
health_url="http://${host}:${port}/healthz"
|
||||
log_file="$RUNNER_TEMP/local-mcp-server.log"
|
||||
pid_file="$RUNNER_TEMP/local-mcp-server.pid"
|
||||
rm -f "$log_file" "$pid_file"
|
||||
|
||||
server_pid="$(
|
||||
python3 - "$GITHUB_WORKSPACE/python" "$log_file" "$host" "$port" "$mount_path" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
workspace, log_file, host, port, mount_path = sys.argv[1:]
|
||||
|
||||
with open(log_file, "w", encoding="utf-8") as log:
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"python",
|
||||
"scripts/local_mcp_streamable_http_server.py",
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
port,
|
||||
"--mount-path",
|
||||
mount_path,
|
||||
],
|
||||
cwd=workspace,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
print(process.pid)
|
||||
PY
|
||||
)"
|
||||
echo "$server_pid" > "$pid_file"
|
||||
|
||||
started=false
|
||||
for _ in $(seq 1 30); do
|
||||
if curl --silent --fail "$health_url" >/dev/null; then
|
||||
started=true
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "$started" == "true" ]]; then
|
||||
if ! (
|
||||
cd "$GITHUB_WORKSPACE/python"
|
||||
LOCAL_MCP_URL="$local_url" uv run python - <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Content, MCPStreamableHTTPTool
|
||||
|
||||
|
||||
def result_to_text(result: str | list[Content]) -> str:
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
return "\n".join(content.text for content in result if content.type == "text" and content.text)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
tool = MCPStreamableHTTPTool(
|
||||
name="local_ci_mcp",
|
||||
url=os.environ["LOCAL_MCP_URL"],
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
async with tool:
|
||||
assert tool.functions, "Local MCP server did not expose any tools."
|
||||
result = result_to_text(await tool.functions[0].invoke(query="What is Agent Framework?"))
|
||||
assert result, "Local MCP server returned an empty response."
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
PY
|
||||
); then
|
||||
started=false
|
||||
fi
|
||||
fi
|
||||
|
||||
effective_url="$local_url"
|
||||
pid="$server_pid"
|
||||
|
||||
if [[ "$started" != "true" ]]; then
|
||||
effective_url="$fallback_url"
|
||||
pid=""
|
||||
if kill -0 "$server_pid" 2>/dev/null; then
|
||||
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" || true
|
||||
sleep 1
|
||||
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" || true
|
||||
fi
|
||||
echo "Local MCP server was unavailable; continuing with fallback LOCAL_MCP_URL."
|
||||
if [[ -f "$log_file" ]]; then
|
||||
tail -n 100 "$log_file" || true
|
||||
fi
|
||||
else
|
||||
echo "Using local MCP server at $local_url"
|
||||
fi
|
||||
|
||||
echo "started=$started" >> "$GITHUB_OUTPUT"
|
||||
echo "local_url=$local_url" >> "$GITHUB_OUTPUT"
|
||||
echo "effective_url=$effective_url" >> "$GITHUB_OUTPUT"
|
||||
echo "pid=$pid" >> "$GITHUB_OUTPUT"
|
||||
@@ -1,19 +1,67 @@
|
||||
# GitHub Copilot Instructions
|
||||
|
||||
Microsoft Agent Framework - a multi-language framework for building, orchestrating, and deploying AI agents.
|
||||
This repository contains both Python and C# code.
|
||||
All python code resides under the `python/` directory.
|
||||
All C# code resides under the `dotnet/` directory.
|
||||
|
||||
## Repository Structure
|
||||
The purpose of the code is to provide a framework for building AI agents.
|
||||
|
||||
- `python/` - Python implementation → see [python/AGENTS.md](../python/AGENTS.md)
|
||||
- `dotnet/` - C#/.NET implementation → see [dotnet/AGENTS.md](../dotnet/AGENTS.md)
|
||||
- `docs/` - Design documents and architectural decision records
|
||||
When contributing to this repository, please follow these guidelines:
|
||||
|
||||
## Architectural Decision Records (ADRs)
|
||||
## C# Code Guidelines
|
||||
|
||||
ADRs in `docs/decisions/` capture significant design decisions and their rationale. They document considered alternatives, trade-offs, and the reasoning behind choices.
|
||||
Here are some general guidelines that apply to all code.
|
||||
|
||||
**Templates:**
|
||||
- `adr-template.md` - Full template with detailed sections
|
||||
- `adr-short-template.md` - Abbreviated template for simpler decisions
|
||||
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
- All public methods and classes should have XML documentation comments.
|
||||
|
||||
When proposing architectural changes, create an ADR to capture options considered and the decision rationale. See [docs/decisions/README.md](../docs/decisions/README.md) for the full process.
|
||||
### C# Sample Code Guidelines
|
||||
|
||||
Sample code is located in the `dotnet/samples` directory.
|
||||
|
||||
When adding a new sample, follow these steps:
|
||||
|
||||
- The sample should be a standalone .net project in one of the subdirectories of the samples directory.
|
||||
- The directory name should be the same as the project name.
|
||||
- The directory should contain a README.md file that explains what the sample does and how to run it.
|
||||
- The README.md file should follow the same format as other samples.
|
||||
- The csproj file should match the directory name.
|
||||
- The csproj file should be configured in the same way as other samples.
|
||||
- The project should preferably contain a single Program.cs file that contains all the sample code.
|
||||
- The sample should be added to the solution file in the samples directory.
|
||||
- The sample should be tested to ensure it works as expected.
|
||||
- A reference to the new samples should be added to the README.md file in the parent directory of the new sample.
|
||||
|
||||
The sample code should follow these guidelines:
|
||||
|
||||
- Configuration settings should be read from environment variables, e.g. `var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");`.
|
||||
- Environment variables should use upper snake_case naming convention.
|
||||
- Secrets should not be hardcoded in the code or committed to the repository.
|
||||
- The code should be well-documented with comments explaining the purpose of each step.
|
||||
- The code should be simple and to the point, avoiding unnecessary complexity.
|
||||
- Prefer inline literals over constants for values that are not reused. For example, use `new ChatClientAgent(chatClient, instructions: "You are a helpful assistant.")` instead of defining a constant for "instructions".
|
||||
- Ensure that all private classes are sealed
|
||||
- Use the Async suffix on the name of all async methods that return a Task or ValueTask.
|
||||
- Prefer defining variables using types rather than var, to help users understand the types involved.
|
||||
- Follow the patterns in the samples in the same directories where new samples are being added.
|
||||
- The structure of the sample should be as follows:
|
||||
- The top of the Program.cs should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
- Then add a comment describing what the sample is demonstrating.
|
||||
- Then add the necessary using statements.
|
||||
- Then add the main code logic.
|
||||
- Finally, add any helper methods or classes at the bottom of the file.
|
||||
|
||||
### C# Unit Test Guidelines
|
||||
|
||||
Unit tests are located in the `dotnet/tests` directory in projects with a `.UnitTests.csproj` suffix.
|
||||
|
||||
Unit tests should follow these guidelines:
|
||||
|
||||
- Use `this.` for accessing class members
|
||||
- Add Arrange, Act and Assert comments for each test
|
||||
- Ensure that all private classes, that are not subclassed, are sealed
|
||||
- Use the Async suffix on the name of all async methods
|
||||
- Use the Moq library for mocking objects where possible
|
||||
- Validate that each test actually tests the target behavior, e.g. we should not have tests that creates a mock, calls the mock and then verifies that the mock was called, without the target code being involved. We also shouldn't have tests that test language features, e.g. something that the compiler would catch anyway.
|
||||
- Avoid adding excessive comments to tests. Instead favour clear easy to understand code.
|
||||
- Follow the patterns in the unit tests in the same project or classes to which new tests are being added
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ workflows:
|
||||
- any-glob-to-any-file:
|
||||
- dotnet/src/Microsoft.Agents.AI.Workflows/**
|
||||
- dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/**
|
||||
- dotnet/samples/03-workflows/**
|
||||
- dotnet/samples/GettingStarted/Workflow/**
|
||||
- python/packages/main/agent_framework/_workflow/**
|
||||
- python/samples/getting_started/workflow/**
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/**
|
||||
* Resolve the issue author and check their team membership.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.github - Octokit REST client from actions/github-script
|
||||
* @param {object} opts.context - GitHub Actions context
|
||||
* @param {object} opts.core - GitHub Actions core toolkit
|
||||
* @param {string} opts.teamSlug - Team slug to check membership against
|
||||
* @param {string|number} opts.issueNumber - Issue number to resolve author for
|
||||
* @returns {Promise<{author: string|null, isTeamMember: boolean}>}
|
||||
*/
|
||||
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber }) {
|
||||
let author = context.payload.issue?.user?.login;
|
||||
if (!author) {
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number(issueNumber),
|
||||
});
|
||||
author = issue.user?.login;
|
||||
}
|
||||
|
||||
if (!author) {
|
||||
core.setFailed('Could not determine issue author (user may be deleted).');
|
||||
return { author: null, isTeamMember: false };
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.teams.getByName({
|
||||
org: context.repo.owner,
|
||||
team_slug: teamSlug,
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(`Team lookup failed for ${teamSlug}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
let isTeamMember = false;
|
||||
try {
|
||||
const teamMembership = await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: context.repo.owner,
|
||||
team_slug: teamSlug,
|
||||
username: author,
|
||||
});
|
||||
isTeamMember = teamMembership.data.state === 'active';
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info(`Author ${author} is not a member of team ${teamSlug}.`);
|
||||
isTeamMember = false;
|
||||
} else {
|
||||
core.setFailed(`Team membership lookup failed for ${author}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return { author, isTeamMember };
|
||||
}
|
||||
|
||||
module.exports = checkTeamMembership;
|
||||
@@ -1,216 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Scan open issues and PRs labeled 'waiting-for-author' for stale follow-ups.
|
||||
|
||||
Team members manually add the 'waiting-for-author' label when they need a
|
||||
response from the external author. If the author hasn't replied within
|
||||
DAYS_THRESHOLD days of the last team comment, post a reminder and add the
|
||||
'requested-info' label to prevent duplicate pings.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from github import Auth, Github, GithubException
|
||||
from github.Issue import Issue
|
||||
from github.IssueComment import IssueComment
|
||||
|
||||
|
||||
PING_COMMENT = (
|
||||
"@{author}, friendly reminder — this issue is waiting on your response. "
|
||||
"Please share any updates when you get a chance. (This is an automated message.)"
|
||||
)
|
||||
TRIGGER_LABEL = "waiting-for-author"
|
||||
PINGED_LABEL = "requested-info"
|
||||
|
||||
|
||||
def get_team_members(g: Github, org: str, team_slug: str) -> set[str]:
|
||||
"""Fetch active team member usernames."""
|
||||
try:
|
||||
org_obj = g.get_organization(org)
|
||||
team = org_obj.get_team_by_slug(team_slug)
|
||||
return {m.login for m in team.get_members()}
|
||||
except GithubException as exc:
|
||||
if exc.status in (403, 404):
|
||||
print(
|
||||
f"ERROR: Failed to fetch team members for {org}/{team_slug} "
|
||||
f"(HTTP {exc.status}). Check that the token has the 'read:org' "
|
||||
f"scope and that the team slug '{team_slug}' is correct."
|
||||
)
|
||||
else:
|
||||
print(f"ERROR: Failed to fetch team members for {org}/{team_slug}: {exc}")
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
print(f"ERROR: Failed to fetch team members for {org}/{team_slug}: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def find_last_team_comment(
|
||||
comments: list[IssueComment], team_members: set[str]
|
||||
) -> IssueComment | None:
|
||||
"""Return the most recent comment from a team member, or None."""
|
||||
for comment in reversed(comments):
|
||||
if comment.user and comment.user.login in team_members:
|
||||
return comment
|
||||
return None
|
||||
|
||||
|
||||
def author_replied_after(
|
||||
comments: list[IssueComment], author: str, after: datetime
|
||||
) -> bool:
|
||||
"""Check if the issue author commented after the given timestamp."""
|
||||
for comment in comments:
|
||||
if (
|
||||
comment.user
|
||||
and comment.user.login == author
|
||||
and comment.created_at > after
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def should_ping(
|
||||
issue: Issue,
|
||||
team_members: set[str],
|
||||
days_threshold: int,
|
||||
now: datetime,
|
||||
) -> bool:
|
||||
"""Determine whether this issue/PR should be pinged.
|
||||
|
||||
Only issues/PRs carrying the 'waiting-for-author' label are candidates.
|
||||
"""
|
||||
author = issue.user.login
|
||||
|
||||
# Skip if the trigger label is not present
|
||||
if not any(label.name == TRIGGER_LABEL for label in issue.labels):
|
||||
return False
|
||||
# Skip if author is a team member
|
||||
if author in team_members:
|
||||
return False
|
||||
|
||||
# Skip if already pinged
|
||||
if any(label.name == PINGED_LABEL for label in issue.labels):
|
||||
return False
|
||||
|
||||
# Skip if no comments at all
|
||||
if issue.comments == 0:
|
||||
return False
|
||||
|
||||
# Fetch comments once for both lookups
|
||||
comments = list(issue.get_comments())
|
||||
|
||||
# Find last team member comment
|
||||
last_team_comment = find_last_team_comment(comments, team_members)
|
||||
if last_team_comment is None:
|
||||
return False
|
||||
|
||||
# Skip if author replied after the last team comment
|
||||
if author_replied_after(comments, author, last_team_comment.created_at):
|
||||
return False
|
||||
|
||||
# Check if enough days have passed
|
||||
days_since = (now - last_team_comment.created_at.astimezone(timezone.utc)).days
|
||||
if days_since < days_threshold:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def ping(issue: Issue, dry_run: bool) -> bool:
|
||||
"""Post a reminder comment and add the 'requested-info' label. Returns True on success."""
|
||||
author = issue.user.login
|
||||
kind = "PR" if issue.pull_request else "Issue"
|
||||
|
||||
if dry_run:
|
||||
print(f" [DRY RUN] Would ping {kind} #{issue.number} (@{author})")
|
||||
return True
|
||||
|
||||
max_retries = 3
|
||||
commented = False
|
||||
labeled = False
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
if not commented:
|
||||
issue.create_comment(PING_COMMENT.format(author=author))
|
||||
commented = True
|
||||
if not labeled:
|
||||
issue.add_to_labels(PINGED_LABEL)
|
||||
labeled = True
|
||||
print(f" Pinged {kind} #{issue.number} (@{author})")
|
||||
return True
|
||||
except Exception as exc:
|
||||
if attempt < max_retries:
|
||||
wait = 2 ** attempt # 2s, 4s
|
||||
print(f" WARN: Attempt {attempt}/{max_retries} failed for {kind} #{issue.number}: {exc}. Retrying in {wait}s...")
|
||||
time.sleep(wait)
|
||||
else:
|
||||
print(f" ERROR: Failed to ping {kind} #{issue.number} after {max_retries} attempts: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
if not token:
|
||||
print("ERROR: GITHUB_TOKEN environment variable is required")
|
||||
sys.exit(1)
|
||||
|
||||
repository = os.environ.get("GITHUB_REPOSITORY")
|
||||
if not repository:
|
||||
print("ERROR: GITHUB_REPOSITORY environment variable is required")
|
||||
sys.exit(1)
|
||||
|
||||
team_slug = os.environ.get("TEAM_SLUG")
|
||||
if not team_slug:
|
||||
print("ERROR: TEAM_SLUG environment variable is required")
|
||||
sys.exit(1)
|
||||
|
||||
days_threshold_raw = os.environ.get("DAYS_THRESHOLD", "4")
|
||||
try:
|
||||
days_threshold = int(days_threshold_raw)
|
||||
except ValueError:
|
||||
print(f"ERROR: DAYS_THRESHOLD must be a numeric value, got '{days_threshold_raw}'")
|
||||
sys.exit(1)
|
||||
dry_run = os.environ.get("DRY_RUN", "false").lower() == "true"
|
||||
|
||||
org = repository.split("/")[0]
|
||||
|
||||
if dry_run:
|
||||
print("Running in DRY RUN mode — no comments or labels will be applied.\n")
|
||||
|
||||
g = Github(auth=Auth.Token(token))
|
||||
repo = g.get_repo(repository)
|
||||
|
||||
print(f"Fetching team members for {org}/{team_slug}...")
|
||||
team_members = get_team_members(g, org, team_slug)
|
||||
print(f"Found {len(team_members)} team members.\n")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
pinged = []
|
||||
failed = []
|
||||
scanned = 0
|
||||
|
||||
print(f"Scanning open issues and PRs labeled '{TRIGGER_LABEL}' (threshold: {days_threshold} days)...\n")
|
||||
|
||||
for issue in repo.get_issues(state="open", labels=[TRIGGER_LABEL]):
|
||||
scanned += 1
|
||||
|
||||
if should_ping(issue, team_members, days_threshold, now):
|
||||
if ping(issue, dry_run):
|
||||
pinged.append(issue.number)
|
||||
else:
|
||||
failed.append(issue.number)
|
||||
|
||||
print(f"\nDone. Scanned {scanned} items, pinged {len(pinged)}, failed {len(failed)}.")
|
||||
if pinged:
|
||||
print(f"Pinged: {', '.join(f'#{n}' for n in pinged)}")
|
||||
if failed:
|
||||
print(f"Failed: {', '.join(f'#{n}' for n in failed)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/**
|
||||
* Tests for check_team_membership.js.
|
||||
*
|
||||
* Run with: node --test .github/tests/test_check_team_membership.js
|
||||
*/
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const checkTeamMembership = require('../scripts/check_team_membership.js');
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState = 'active' } = {}) {
|
||||
const core = {
|
||||
_infoMessages: [],
|
||||
_failedMessages: [],
|
||||
info(msg) { this._infoMessages.push(msg); },
|
||||
setFailed(msg) { this._failedMessages.push(msg); },
|
||||
};
|
||||
|
||||
const context = {
|
||||
payload: { issue: payloadIssue },
|
||||
repo: { owner: 'test-org', repo: 'test-repo' },
|
||||
};
|
||||
|
||||
const github = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: async () => ({
|
||||
data: { user: apiUser ? { login: apiUser } : null },
|
||||
}),
|
||||
},
|
||||
teams: {
|
||||
getByName: async () => ({}),
|
||||
getMembershipForUserInOrg: async () => ({
|
||||
data: { state: teamState },
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { core, context, github };
|
||||
}
|
||||
|
||||
const BASE_OPTS = { teamSlug: 'my-team', issueNumber: '123' };
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Author resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('author resolution', () => {
|
||||
it('resolves author from event payload', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'payload-user' } },
|
||||
});
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.author, 'payload-user');
|
||||
});
|
||||
|
||||
it('resolves author via API when payload issue is absent', async () => {
|
||||
const { github, context, core } = createMocks({ apiUser: 'api-user' });
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.author, 'api-user');
|
||||
});
|
||||
|
||||
it('resolves author via API when payload issue user is null (deleted account)', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: null },
|
||||
apiUser: 'fetched-user',
|
||||
});
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.author, 'fetched-user');
|
||||
});
|
||||
|
||||
it('handles deleted account when API also returns null user', async () => {
|
||||
const { github, context, core } = createMocks({ apiUser: null });
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.author, null);
|
||||
assert.equal(result.isTeamMember, false);
|
||||
assert.ok(core._failedMessages.some(m => m.includes('deleted')));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Team lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('team lookup', () => {
|
||||
it('fails the job when team lookup errors', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'user1' } },
|
||||
});
|
||||
const error = new Error('Bad credentials');
|
||||
github.rest.teams.getByName = async () => { throw error; };
|
||||
|
||||
await assert.rejects(
|
||||
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
|
||||
(err) => err === error,
|
||||
);
|
||||
assert.ok(core._failedMessages.some(m => m.includes('Team lookup failed')));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Team membership
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('team membership', () => {
|
||||
it('returns true for active team member', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'member' } },
|
||||
teamState: 'active',
|
||||
});
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.isTeamMember, true);
|
||||
});
|
||||
|
||||
it('returns false for pending team member', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'pending-user' } },
|
||||
teamState: 'pending',
|
||||
});
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.isTeamMember, false);
|
||||
});
|
||||
|
||||
it('treats 404 membership response as non-member without failing', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'outsider' } },
|
||||
});
|
||||
const notFoundError = new Error('Not Found');
|
||||
notFoundError.status = 404;
|
||||
github.rest.teams.getMembershipForUserInOrg = async () => { throw notFoundError; };
|
||||
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.isTeamMember, false);
|
||||
assert.equal(core._failedMessages.length, 0);
|
||||
assert.ok(core._infoMessages.some(m => m.includes('not a member')));
|
||||
});
|
||||
|
||||
it('fails the job on non-404 membership errors', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'user1' } },
|
||||
});
|
||||
const serverError = new Error('Internal Server Error');
|
||||
serverError.status = 500;
|
||||
github.rest.teams.getMembershipForUserInOrg = async () => { throw serverError; };
|
||||
|
||||
await assert.rejects(
|
||||
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
|
||||
(err) => err === serverError,
|
||||
);
|
||||
assert.ok(core._failedMessages.some(m => m.includes('membership lookup failed')));
|
||||
});
|
||||
|
||||
it('fails the job on membership errors without status code', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadIssue: { user: { login: 'user1' } },
|
||||
});
|
||||
const networkError = new Error('ECONNREFUSED');
|
||||
github.rest.teams.getMembershipForUserInOrg = async () => { throw networkError; };
|
||||
|
||||
await assert.rejects(
|
||||
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
|
||||
(err) => err === networkError,
|
||||
);
|
||||
assert.ok(core._failedMessages.some(m => m.includes('membership lookup failed')));
|
||||
});
|
||||
});
|
||||
@@ -1,297 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Tests for stale_issue_pr_ping.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Ensure the script directory is importable
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||||
|
||||
from stale_issue_pr_ping import (
|
||||
PINGED_LABEL,
|
||||
PING_COMMENT,
|
||||
TRIGGER_LABEL,
|
||||
author_replied_after,
|
||||
find_last_team_comment,
|
||||
get_team_members,
|
||||
main,
|
||||
ping,
|
||||
should_ping,
|
||||
)
|
||||
|
||||
TEAM = {"alice", "bob"}
|
||||
NOW = datetime(2026, 3, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_comment(login: str | None, created_at: datetime) -> MagicMock:
|
||||
"""Create a mock IssueComment."""
|
||||
c = MagicMock()
|
||||
if login is None:
|
||||
c.user = None
|
||||
else:
|
||||
c.user = MagicMock()
|
||||
c.user.login = login
|
||||
c.created_at = created_at
|
||||
return c
|
||||
|
||||
|
||||
def _make_label(name: str) -> MagicMock:
|
||||
lbl = MagicMock()
|
||||
lbl.name = name
|
||||
return lbl
|
||||
|
||||
|
||||
def _make_issue(
|
||||
author: str = "external",
|
||||
labels: list[str] | None = None,
|
||||
comment_count: int = 1,
|
||||
comments: list[MagicMock] | None = None,
|
||||
pull_request: bool = False,
|
||||
number: int = 42,
|
||||
) -> MagicMock:
|
||||
issue = MagicMock()
|
||||
issue.user = MagicMock()
|
||||
issue.user.login = author
|
||||
issue.number = number
|
||||
# Default to having the trigger label, since the API query pre-filters.
|
||||
if labels is None:
|
||||
labels = [TRIGGER_LABEL]
|
||||
issue.labels = [_make_label(n) for n in labels]
|
||||
issue.comments = comment_count
|
||||
issue.pull_request = MagicMock() if pull_request else None
|
||||
if comments is not None:
|
||||
issue.get_comments.return_value = comments
|
||||
return issue
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# find_last_team_comment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFindLastTeamComment:
|
||||
def test_returns_last_team_comment(self):
|
||||
c1 = _make_comment("alice", datetime(2026, 3, 1, tzinfo=timezone.utc))
|
||||
c2 = _make_comment("external", datetime(2026, 3, 2, tzinfo=timezone.utc))
|
||||
c3 = _make_comment("bob", datetime(2026, 3, 3, tzinfo=timezone.utc))
|
||||
assert find_last_team_comment([c1, c2, c3], TEAM) is c3
|
||||
|
||||
def test_returns_none_when_no_team_comments(self):
|
||||
c1 = _make_comment("external", datetime(2026, 3, 1, tzinfo=timezone.utc))
|
||||
assert find_last_team_comment([c1], TEAM) is None
|
||||
|
||||
def test_returns_none_for_empty_list(self):
|
||||
assert find_last_team_comment([], TEAM) is None
|
||||
|
||||
def test_skips_deleted_user(self):
|
||||
c1 = _make_comment(None, datetime(2026, 3, 1, tzinfo=timezone.utc))
|
||||
c2 = _make_comment("alice", datetime(2026, 3, 2, tzinfo=timezone.utc))
|
||||
assert find_last_team_comment([c1, c2], TEAM) is c2
|
||||
|
||||
def test_only_deleted_users(self):
|
||||
c1 = _make_comment(None, datetime(2026, 3, 1, tzinfo=timezone.utc))
|
||||
assert find_last_team_comment([c1], TEAM) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# author_replied_after
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAuthorRepliedAfter:
|
||||
def test_author_replied(self):
|
||||
after = datetime(2026, 3, 1, tzinfo=timezone.utc)
|
||||
c1 = _make_comment("external", datetime(2026, 3, 2, tzinfo=timezone.utc))
|
||||
assert author_replied_after([c1], "external", after) is True
|
||||
|
||||
def test_author_not_replied(self):
|
||||
after = datetime(2026, 3, 5, tzinfo=timezone.utc)
|
||||
c1 = _make_comment("external", datetime(2026, 3, 2, tzinfo=timezone.utc))
|
||||
assert author_replied_after([c1], "external", after) is False
|
||||
|
||||
def test_different_user_replied(self):
|
||||
after = datetime(2026, 3, 1, tzinfo=timezone.utc)
|
||||
c1 = _make_comment("someone_else", datetime(2026, 3, 2, tzinfo=timezone.utc))
|
||||
assert author_replied_after([c1], "external", after) is False
|
||||
|
||||
def test_deleted_user_comment(self):
|
||||
after = datetime(2026, 3, 1, tzinfo=timezone.utc)
|
||||
c1 = _make_comment(None, datetime(2026, 3, 2, tzinfo=timezone.utc))
|
||||
assert author_replied_after([c1], "external", after) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# should_ping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestShouldPing:
|
||||
def test_should_ping_stale_issue(self):
|
||||
team_comment = _make_comment("alice", NOW - timedelta(days=5))
|
||||
issue = _make_issue(comments=[team_comment], comment_count=1)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is True
|
||||
|
||||
def test_skip_team_member_author(self):
|
||||
issue = _make_issue(author="alice", labels=[TRIGGER_LABEL], comment_count=1)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is False
|
||||
|
||||
def test_skip_already_pinged(self):
|
||||
issue = _make_issue(labels=[TRIGGER_LABEL, PINGED_LABEL], comment_count=1)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is False
|
||||
|
||||
def test_skip_no_comments(self):
|
||||
issue = _make_issue(comment_count=0)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is False
|
||||
|
||||
def test_skip_no_team_comment(self):
|
||||
c = _make_comment("external", NOW - timedelta(days=5))
|
||||
issue = _make_issue(comments=[c], comment_count=1)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is False
|
||||
|
||||
def test_skip_author_replied(self):
|
||||
team_c = _make_comment("alice", NOW - timedelta(days=5))
|
||||
author_c = _make_comment("external", NOW - timedelta(days=3))
|
||||
issue = _make_issue(comments=[team_c, author_c], comment_count=2)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is False
|
||||
|
||||
def test_skip_not_enough_days(self):
|
||||
team_comment = _make_comment("alice", NOW - timedelta(days=2))
|
||||
issue = _make_issue(comments=[team_comment], comment_count=1)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is False
|
||||
|
||||
def test_aware_datetime_handled(self):
|
||||
"""Timezone-aware datetimes should not be mangled by astimezone."""
|
||||
aware_dt = (NOW - timedelta(days=5)).replace(tzinfo=timezone.utc)
|
||||
team_comment = _make_comment("alice", aware_dt)
|
||||
issue = _make_issue(comments=[team_comment], comment_count=1)
|
||||
assert should_ping(issue, TEAM, 4, NOW) is True
|
||||
|
||||
def test_naive_datetime_handled(self):
|
||||
"""Naive datetimes (pre-PyGithub 2.x) should be handled by astimezone."""
|
||||
naive_dt = (NOW - timedelta(days=5)).replace(tzinfo=None)
|
||||
team_comment = _make_comment("alice", naive_dt)
|
||||
issue = _make_issue(comments=[team_comment], comment_count=1)
|
||||
# astimezone on naive datetime treats it as local time; just verify no crash
|
||||
should_ping(issue, TEAM, 4, NOW)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPing:
|
||||
def test_dry_run(self, capsys):
|
||||
issue = _make_issue()
|
||||
assert ping(issue, dry_run=True) is True
|
||||
issue.create_comment.assert_not_called()
|
||||
assert "DRY RUN" in capsys.readouterr().out
|
||||
|
||||
def test_success(self, capsys):
|
||||
issue = _make_issue()
|
||||
assert ping(issue, dry_run=False) is True
|
||||
issue.create_comment.assert_called_once()
|
||||
issue.add_to_labels.assert_called_once_with(PINGED_LABEL)
|
||||
|
||||
@patch("stale_issue_pr_ping.time.sleep")
|
||||
def test_retry_on_failure(self, mock_sleep):
|
||||
issue = _make_issue()
|
||||
issue.create_comment.side_effect = [Exception("net error"), None]
|
||||
assert ping(issue, dry_run=False) is True
|
||||
assert issue.create_comment.call_count == 2
|
||||
mock_sleep.assert_called_once()
|
||||
|
||||
@patch("stale_issue_pr_ping.time.sleep")
|
||||
def test_idempotent_retry_skips_comment_on_label_failure(self, mock_sleep):
|
||||
"""If create_comment succeeds but add_to_labels fails, retry should not re-comment."""
|
||||
issue = _make_issue()
|
||||
issue.add_to_labels.side_effect = [Exception("label error"), None]
|
||||
assert ping(issue, dry_run=False) is True
|
||||
# Comment should only be created once even though there were 2 attempts
|
||||
assert issue.create_comment.call_count == 1
|
||||
assert issue.add_to_labels.call_count == 2
|
||||
|
||||
@patch("stale_issue_pr_ping.time.sleep")
|
||||
def test_all_retries_fail(self, mock_sleep):
|
||||
issue = _make_issue()
|
||||
issue.create_comment.side_effect = Exception("permanent error")
|
||||
assert ping(issue, dry_run=False) is False
|
||||
assert issue.create_comment.call_count == 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_team_members
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGetTeamMembers:
|
||||
def test_success(self):
|
||||
g = MagicMock()
|
||||
member = MagicMock()
|
||||
member.login = "alice"
|
||||
g.get_organization.return_value.get_team_by_slug.return_value.get_members.return_value = [member]
|
||||
assert get_team_members(g, "org", "my-team") == {"alice"}
|
||||
|
||||
def test_403_error_message(self, capsys):
|
||||
from github import GithubException
|
||||
|
||||
g = MagicMock()
|
||||
g.get_organization.return_value.get_team_by_slug.side_effect = GithubException(
|
||||
403, {"message": "Forbidden"}, None
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
get_team_members(g, "org", "my-team")
|
||||
out = capsys.readouterr().out
|
||||
assert "read:org" in out
|
||||
assert "403" in out
|
||||
|
||||
def test_404_error_message(self, capsys):
|
||||
from github import GithubException
|
||||
|
||||
g = MagicMock()
|
||||
g.get_organization.return_value.get_team_by_slug.side_effect = GithubException(
|
||||
404, {"message": "Not Found"}, None
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
get_team_members(g, "org", "bad-slug")
|
||||
out = capsys.readouterr().out
|
||||
assert "read:org" in out
|
||||
assert "bad-slug" in out
|
||||
|
||||
def test_generic_error(self, capsys):
|
||||
g = MagicMock()
|
||||
g.get_organization.side_effect = RuntimeError("boom")
|
||||
with pytest.raises(SystemExit):
|
||||
get_team_members(g, "org", "team")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main – env var validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMain:
|
||||
@patch.dict(os.environ, {
|
||||
"GITHUB_TOKEN": "tok",
|
||||
"GITHUB_REPOSITORY": "org/repo",
|
||||
"TEAM_SLUG": "my-team",
|
||||
"DAYS_THRESHOLD": "abc",
|
||||
}, clear=True)
|
||||
def test_invalid_days_threshold(self, capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "numeric" in capsys.readouterr().out
|
||||
|
||||
@patch.dict(os.environ, {
|
||||
"GITHUB_TOKEN": "tok",
|
||||
"GITHUB_REPOSITORY": "org/repo",
|
||||
}, clear=True)
|
||||
def test_missing_team_slug(self, capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "TEAM_SLUG" in capsys.readouterr().out
|
||||
@@ -1,165 +0,0 @@
|
||||
name: DevFlow PR Review
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- ready_for_review
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request number to review
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: devflow-pr-review-${{ github.repository }}-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DEVFLOW_REPOSITORY: ${{ vars.DF_REPO }}
|
||||
DEVFLOW_REF: main
|
||||
TARGET_REPO_PATH: ${{ github.workspace }}/target-repo
|
||||
DEVFLOW_PATH: ${{ github.workspace }}/devflow
|
||||
|
||||
jobs:
|
||||
team_check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_team_member: ${{ steps.check.outputs.is_team_member }}
|
||||
pr_number: ${{ steps.pr.outputs.pr_number }}
|
||||
pr_url: ${{ steps.pr.outputs.pr_url }}
|
||||
repo: ${{ steps.pr.outputs.repo }}
|
||||
steps:
|
||||
- name: Resolve PR metadata
|
||||
id: pr
|
||||
shell: bash
|
||||
env:
|
||||
PR_HTML_URL: ${{ github.event.pull_request.html_url }}
|
||||
PR_NUMBER_EVENT: ${{ github.event.pull_request.number }}
|
||||
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
||||
pr_number="${PR_NUMBER_EVENT}"
|
||||
pr_url="${PR_HTML_URL}"
|
||||
else
|
||||
pr_number="${PR_NUMBER_INPUT}"
|
||||
pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${pr_number}"
|
||||
fi
|
||||
|
||||
if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Could not determine PR number; for workflow_dispatch runs, the 'pr_number' input is required when not running on pull_request_target." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pr_url=${pr_url}" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
|
||||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check PR author team membership
|
||||
id: check
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
|
||||
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
script: |
|
||||
let author = context.payload.pull_request?.user?.login;
|
||||
if (!author) {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: Number(process.env.PR_NUMBER),
|
||||
});
|
||||
author = pr.user.login;
|
||||
}
|
||||
|
||||
let isTeamMember = false;
|
||||
try {
|
||||
const teamMembership = await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: context.repo.owner,
|
||||
team_slug: process.env.TEAM_NAME,
|
||||
username: author,
|
||||
});
|
||||
isTeamMember = teamMembership.data.state === 'active';
|
||||
} catch (error) {
|
||||
console.log(`Team membership lookup failed for ${author}: ${error.message}`);
|
||||
isTeamMember = false;
|
||||
}
|
||||
|
||||
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
|
||||
if (isTeamMember) {
|
||||
core.info(`Author ${author} is a team member; proceeding with review.`);
|
||||
} else {
|
||||
core.info(`Author ${author} is not a member of ${process.env.TEAM_NAME}; skipping review.`);
|
||||
}
|
||||
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
needs: team_check
|
||||
if: ${{ needs.team_check.outputs.is_team_member == 'true' }}
|
||||
timeout-minutes: 60
|
||||
# Advisory check: failures here should not block the PR. The reviewer
|
||||
# posts comments as a best-effort signal; if the pipeline breaks, the
|
||||
# PR author should still be able to merge without a red required check.
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
# Safe checkout: base repo only, not the untrusted PR head.
|
||||
- name: Checkout target repo base
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
path: target-repo
|
||||
|
||||
# Private DevFlow checkout: the PAT/token grants access to this repo's code.
|
||||
- name: Checkout DevFlow
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ env.DEVFLOW_REPOSITORY }}
|
||||
ref: ${{ env.DEVFLOW_REF }}
|
||||
token: ${{ secrets.DEVFLOW_TOKEN }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
path: devflow
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install DevFlow dependencies
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Run PR review
|
||||
id: review
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_COPILOT_TOKEN: ${{ secrets.GH_COPILOT_TOKEN }}
|
||||
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
PR_URL: ${{ needs.team_check.outputs.pr_url }}
|
||||
run: |
|
||||
uv run python scripts/trigger_pr_review.py \
|
||||
--pr-url "$PR_URL" \
|
||||
--github-username "$GITHUB_ACTOR" \
|
||||
--no-require-comment-selection
|
||||
@@ -59,20 +59,20 @@ jobs:
|
||||
if: steps.filter.outputs.dotnet != 'true'
|
||||
run: echo "NOT dotnet file"
|
||||
|
||||
# Build the full solution (including samples) on all TFMs. No tests.
|
||||
dotnet-build:
|
||||
dotnet-build-and-test:
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.dotnetChanges == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release }
|
||||
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
- { targetFramework: "net9.0", os: "windows-latest", configuration: Debug }
|
||||
- { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release }
|
||||
- { targetFramework: "net472", os: "windows-latest", configuration: Release }
|
||||
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -82,10 +82,20 @@ jobs:
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
workflow-samples
|
||||
|
||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
uses: actions/setup-dotnet@v5.1.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
- name: Build dotnet solutions
|
||||
@@ -129,107 +139,34 @@ jobs:
|
||||
popd
|
||||
popd
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# Build src+tests only (no samples) for a single TFM and run tests.
|
||||
dotnet-test:
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.dotnetChanges == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
- name: Generate test solution (no samples)
|
||||
shell: pwsh
|
||||
run: |
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 `
|
||||
-Solution dotnet/agent-framework-dotnet.slnx `
|
||||
-TargetFramework ${{ matrix.targetFramework }} `
|
||||
-Configuration ${{ matrix.configuration }} `
|
||||
-ExcludeSamples `
|
||||
-OutputPath dotnet/filtered.slnx `
|
||||
-Verbose
|
||||
|
||||
- name: Build src and tests
|
||||
shell: bash
|
||||
run: dotnet build dotnet/filtered.slnx -c ${{ matrix.configuration }} -f ${{ matrix.targetFramework }} --warnaserror
|
||||
|
||||
- name: Generate test-type filtered solutions
|
||||
shell: pwsh
|
||||
run: |
|
||||
$commonArgs = @{
|
||||
Solution = "dotnet/filtered.slnx"
|
||||
TargetFramework = "${{ matrix.targetFramework }}"
|
||||
Configuration = "${{ matrix.configuration }}"
|
||||
Verbose = $true
|
||||
}
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
|
||||
-TestProjectNameFilter "*UnitTests*" `
|
||||
-OutputPath dotnet/filtered-unit.slnx
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
|
||||
-TestProjectNameFilter "*IntegrationTests*" `
|
||||
-OutputPath dotnet/filtered-integration.slnx
|
||||
|
||||
|
||||
- name: Run Unit Tests
|
||||
shell: pwsh
|
||||
working-directory: dotnet
|
||||
shell: bash
|
||||
run: |
|
||||
$coverageSettings = Join-Path $PWD "tests/coverage.runsettings"
|
||||
$coverageArgs = @()
|
||||
if ("${{ matrix.targetFramework }}" -eq "${{ env.COVERAGE_FRAMEWORK }}") {
|
||||
$coverageArgs = @(
|
||||
"--coverage",
|
||||
"--coverage-output-format", "cobertura",
|
||||
"--coverage-settings", $coverageSettings,
|
||||
"--results-directory", "../TestResults/Coverage/"
|
||||
)
|
||||
}
|
||||
|
||||
dotnet test --solution ./filtered-unit.slnx `
|
||||
-f ${{ matrix.targetFramework }} `
|
||||
-c ${{ matrix.configuration }} `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--ignore-exit-code 8 `
|
||||
@coverageArgs
|
||||
export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | tr '\n' ' ')
|
||||
for project in $UT_PROJECTS; do
|
||||
# Query the project's target frameworks using MSBuild with the current configuration
|
||||
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
|
||||
|
||||
# Check if the project supports the target framework
|
||||
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
|
||||
if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
|
||||
else
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
|
||||
fi
|
||||
else
|
||||
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
|
||||
fi
|
||||
done
|
||||
env:
|
||||
# Cosmos DB Emulator connection settings
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
|
||||
- name: Log event name and matrix integration-tests
|
||||
shell: bash
|
||||
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
|
||||
shell: bash
|
||||
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
|
||||
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request' && matrix.integration-tests
|
||||
@@ -248,48 +185,53 @@ jobs:
|
||||
id: azure-functions-setup
|
||||
|
||||
- name: Run Integration Tests
|
||||
shell: pwsh
|
||||
working-directory: dotnet
|
||||
shell: bash
|
||||
if: github.event_name != 'pull_request' && matrix.integration-tests
|
||||
run: |
|
||||
dotnet test --solution ./filtered-integration.slnx `
|
||||
-f ${{ matrix.targetFramework }} `
|
||||
-c ${{ matrix.configuration }} `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-not-trait "Category=IntegrationDisabled" `
|
||||
--parallel-algorithm aggressive `
|
||||
--max-threads 2.0x
|
||||
export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ')
|
||||
for project in $INTEGRATION_TEST_PROJECTS; do
|
||||
# Query the project's target frameworks using MSBuild with the current configuration
|
||||
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
|
||||
|
||||
# Check if the project supports the target framework
|
||||
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
|
||||
else
|
||||
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
|
||||
fi
|
||||
done
|
||||
env:
|
||||
# Cosmos DB Emulator connection settings
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
# 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 }}
|
||||
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
|
||||
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
|
||||
# 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 }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
# Azure AI 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 }}
|
||||
AzureAI__Endpoint: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
AzureAI__BingConnectionId: ${{ vars.AZUREAI__BINGCONECTIONID }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MEDIA_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MEDIA_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_CONNECTION_GROUNDING_TOOL: ${{ vars.FOUNDRY_CONNECTION_GROUNDING_TOOL }}
|
||||
|
||||
# Generate test reports and check coverage
|
||||
- name: Generate test reports
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.3
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
|
||||
with:
|
||||
reports: "./TestResults/Coverage/**/*.cobertura.xml"
|
||||
reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
|
||||
targetdir: "./TestResults/Reports"
|
||||
reporttypes: "HtmlInline;JsonSummary"
|
||||
|
||||
- name: Upload coverage report artifact
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
|
||||
path: ./TestResults/Reports # Directory containing files to upload
|
||||
@@ -297,13 +239,13 @@ jobs:
|
||||
- name: Check coverage
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
shell: pwsh
|
||||
run: ./dotnet/eng/scripts/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
|
||||
run: .github/workflows/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
|
||||
|
||||
# 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]
|
||||
needs: [dotnet-build-and-test]
|
||||
steps:
|
||||
- name: Get Date
|
||||
shell: bash
|
||||
|
||||
@@ -86,10 +86,11 @@ jobs:
|
||||
run: docker pull mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }}
|
||||
|
||||
# This step will run dotnet format on each of the unique csproj files and fail if any changes are made
|
||||
# exclude-diagnostics should be removed after fixes for IL2026 and IL3050 are out: https://github.com/dotnet/sdk/issues/51136
|
||||
- name: Run dotnet format
|
||||
if: steps.find-csproj.outputs.csproj_files != ''
|
||||
run: |
|
||||
for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
|
||||
echo "Running dotnet format on $csproj"
|
||||
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic"
|
||||
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic --exclude-diagnostics IL2026 IL3050"
|
||||
done
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
#
|
||||
# Dedicated .NET integration tests workflow, called from the manual integration test orchestrator.
|
||||
# Only runs integration test matrix entries (net10.0 and net472).
|
||||
#
|
||||
|
||||
name: dotnet-integration-tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
checkout-ref:
|
||||
description: "Git ref to checkout (e.g., refs/pull/123/head)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
dotnet-integration-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release }
|
||||
- { targetFramework: "net472", os: "windows-latest", configuration: Release }
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Launching Azure Cosmos DB Emulator"
|
||||
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
|
||||
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
- name: Build dotnet solutions
|
||||
shell: bash
|
||||
run: |
|
||||
export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
|
||||
for solution in $SOLUTIONS; do
|
||||
dotnet build $solution -c ${{ matrix.configuration }} --warnaserror
|
||||
done
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@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
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
|
||||
- name: Run Integration Tests
|
||||
shell: bash
|
||||
run: |
|
||||
export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ')
|
||||
for project in $INTEGRATION_TEST_PROJECTS; do
|
||||
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
|
||||
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled"
|
||||
else
|
||||
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
|
||||
fi
|
||||
done
|
||||
env:
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
|
||||
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AzureAI__Endpoint: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
AzureAI__BingConnectionId: ${{ vars.AZUREAI__BINGCONECTIONID }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MEDIA_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MEDIA_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_CONNECTION_GROUNDING_TOOL: ${{ vars.FOUNDRY_CONNECTION_GROUNDING_TOOL }}
|
||||
@@ -1,137 +0,0 @@
|
||||
#
|
||||
# Runs the .NET sample verification tool, which builds and executes sample projects
|
||||
# and verifies their output using deterministic checks and AI-powered verification.
|
||||
#
|
||||
# Results are displayed as a GitHub Job Summary and the CSV report is uploaded as an artifact.
|
||||
#
|
||||
|
||||
name: dotnet-verify-samples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
category:
|
||||
description: "Sample category to run (blank for all)"
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- ""
|
||||
- "01-get-started"
|
||||
- "02-agents"
|
||||
- "03-workflows"
|
||||
parallelism:
|
||||
description: "Max parallel sample runs"
|
||||
required: false
|
||||
default: "8"
|
||||
type: string
|
||||
schedule:
|
||||
- cron: "0 6 * * 1-5" # Weekdays at 6:00 UTC
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
verify-samples:
|
||||
runs-on: ubuntu-latest
|
||||
environment: 'integration'
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Generate filtered solution
|
||||
shell: pwsh
|
||||
run: |
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 `
|
||||
-Solution dotnet/agent-framework-dotnet.slnx `
|
||||
-TargetFramework net10.0 `
|
||||
-Configuration Debug `
|
||||
-OutputPath dotnet/filtered.slnx `
|
||||
-Verbose
|
||||
|
||||
- name: Build solution
|
||||
shell: bash
|
||||
run: dotnet build dotnet/filtered.slnx -f net10.0 --warnaserror
|
||||
|
||||
- name: Run verify-samples
|
||||
id: verify
|
||||
working-directory: dotnet
|
||||
shell: bash
|
||||
run: |
|
||||
CATEGORY_ARG=""
|
||||
if [ -n "$CATEGORY_INPUT" ]; then
|
||||
CATEGORY_ARG="--category $CATEGORY_INPUT"
|
||||
fi
|
||||
|
||||
dotnet run --project eng/verify-samples -- \
|
||||
$CATEGORY_ARG \
|
||||
--parallel "$PARALLELISM" \
|
||||
--md results.md \
|
||||
--csv results.csv \
|
||||
--log results.log
|
||||
env:
|
||||
CATEGORY_INPUT: ${{ github.event.inputs.category || '' }}
|
||||
PARALLELISM: ${{ github.event.inputs.parallelism || '8' }}
|
||||
# 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 }}
|
||||
# Azure AI 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: Write Job Summary
|
||||
if: always()
|
||||
working-directory: dotnet
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f results.md ]; then
|
||||
cat results.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "⚠️ No results.md generated — verify-samples may have failed to start." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: verify-samples-results
|
||||
path: |
|
||||
dotnet/results.csv
|
||||
dotnet/results.log
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Fail if samples failed
|
||||
if: always() && steps.verify.outcome == 'failure'
|
||||
shell: bash
|
||||
run: exit 1
|
||||
@@ -1,134 +0,0 @@
|
||||
#
|
||||
# This workflow allows manually running integration tests against an open PR or a branch.
|
||||
# Go to Actions → "Integration Tests (Manual)" → Run workflow → enter a PR number or branch name.
|
||||
#
|
||||
# It calls dedicated integration-only workflows (dotnet-integration-tests and python-integration-tests),
|
||||
# passing a ref so they check out and test the correct code.
|
||||
# Changed paths are detected here so only the relevant test suites run.
|
||||
#
|
||||
|
||||
name: Integration Tests (Manual)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr-number:
|
||||
description: "PR number to run integration tests against (leave empty if using branch)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
branch:
|
||||
description: "Branch name to run integration tests against (leave empty if using PR number)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: integration-tests-manual-${{ github.event.inputs.pr-number || github.event.inputs.branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
resolve-ref:
|
||||
name: Resolve ref
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
checkout-ref: ${{ steps.resolve.outputs.checkout-ref }}
|
||||
dotnet-changes: ${{ steps.detect-changes.outputs.dotnet }}
|
||||
python-changes: ${{ steps.detect-changes.outputs.python }}
|
||||
steps:
|
||||
- name: Resolve checkout ref
|
||||
id: resolve
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.inputs.pr-number }}
|
||||
BRANCH: ${{ github.event.inputs.branch }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$BRANCH" ]; then
|
||||
echo "::error::Please provide either a PR number or a branch name, not both."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$PR_NUMBER" ] && [ -z "$BRANCH" ]; then
|
||||
echo "::error::Please provide either a PR number or a branch name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
if ! echo "$PR_NUMBER" | grep -Eq '^[0-9]+$'; then
|
||||
echo "::error::Invalid PR number. Only numeric values are allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state)
|
||||
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
|
||||
|
||||
if [ "$PR_STATE" != "OPEN" ]; then
|
||||
echo "::error::PR #$PR_NUMBER is not open (state: $PR_STATE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "checkout-ref=refs/pull/$PR_NUMBER/head" >> "$GITHUB_OUTPUT"
|
||||
echo "Running integration tests for PR #$PR_NUMBER"
|
||||
else
|
||||
if ! echo "$BRANCH" | grep -Eq '^[a-zA-Z0-9_./-]+$'; then
|
||||
echo "::error::Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes are allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "checkout-ref=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
echo "Running integration tests for branch $BRANCH"
|
||||
fi
|
||||
|
||||
- name: Detect changed paths
|
||||
id: detect-changes
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.inputs.pr-number }}
|
||||
BRANCH: ${{ github.event.inputs.branch }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only)
|
||||
else
|
||||
# For branches, compare against main using the GitHub API
|
||||
CHANGED_FILES=$(gh api "repos/$REPO/compare/main...$BRANCH" --jq '.files[].filename')
|
||||
fi
|
||||
|
||||
DOTNET_CHANGES=false
|
||||
PYTHON_CHANGES=false
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q '^dotnet/'; then
|
||||
DOTNET_CHANGES=true
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q '^python/'; then
|
||||
PYTHON_CHANGES=true
|
||||
fi
|
||||
|
||||
echo "dotnet=$DOTNET_CHANGES" >> "$GITHUB_OUTPUT"
|
||||
echo "python=$PYTHON_CHANGES" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected changes — dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
|
||||
|
||||
dotnet-integration-tests:
|
||||
name: .NET Integration Tests
|
||||
needs: resolve-ref
|
||||
if: needs.resolve-ref.outputs.dotnet-changes == 'true'
|
||||
uses: ./.github/workflows/dotnet-integration-tests.yml
|
||||
with:
|
||||
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
|
||||
secrets: inherit
|
||||
|
||||
python-integration-tests:
|
||||
name: Python Integration Tests
|
||||
needs: resolve-ref
|
||||
if: needs.resolve-ref.outputs.python-changes == 'true'
|
||||
uses: ./.github/workflows/python-integration-tests.yml
|
||||
with:
|
||||
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
|
||||
secrets: inherit
|
||||
@@ -1,199 +0,0 @@
|
||||
name: Issue Triage
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: Issue number to triage
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: issue-triage-${{ github.repository }}-${{ github.event.issue.number || inputs.issue_number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DEVFLOW_REPOSITORY: ${{ vars.DF_REPO }}
|
||||
DEVFLOW_REF: main
|
||||
TARGET_REPO_PATH: ${{ github.workspace }}/target-repo
|
||||
DEVFLOW_PATH: ${{ github.workspace }}/devflow
|
||||
|
||||
jobs:
|
||||
team_check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_team_member: ${{ steps.check.outputs.is_team_member }}
|
||||
issue_number: ${{ steps.issue.outputs.issue_number }}
|
||||
repo: ${{ steps.issue.outputs.repo }}
|
||||
steps:
|
||||
- name: Resolve issue metadata
|
||||
id: issue
|
||||
shell: bash
|
||||
env:
|
||||
ISSUE_NUMBER_EVENT: ${{ github.event.issue.number }}
|
||||
ISSUE_NUMBER_INPUT: ${{ inputs.issue_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "issues" ]]; then
|
||||
issue_number="${ISSUE_NUMBER_EVENT}"
|
||||
else
|
||||
issue_number="${ISSUE_NUMBER_INPUT}"
|
||||
fi
|
||||
|
||||
if [[ ! "$issue_number" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Could not determine issue number; for workflow_dispatch runs, the 'issue_number' input is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "issue_number=${issue_number}" >> "$GITHUB_OUTPUT"
|
||||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check issue author team membership
|
||||
id: check
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
|
||||
ISSUE_NUMBER: ${{ steps.issue.outputs.issue_number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
script: |
|
||||
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
|
||||
const { author, isTeamMember } = await checkTeamMembership({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
teamSlug: process.env.TEAM_NAME,
|
||||
issueNumber: process.env.ISSUE_NUMBER,
|
||||
});
|
||||
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
|
||||
if (isTeamMember) {
|
||||
core.info(`Author ${author} is a team member; skipping auto-triage.`);
|
||||
} else {
|
||||
core.info(`Author ${author} is not a team member; proceeding with triage.`);
|
||||
}
|
||||
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: team_check
|
||||
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
# Safe checkout: base repo only.
|
||||
- name: Checkout target repo base
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
path: target-repo
|
||||
|
||||
# Private DevFlow (maf-dashboard) checkout.
|
||||
- name: Checkout DevFlow
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ env.DEVFLOW_REPOSITORY }}
|
||||
ref: ${{ env.DEVFLOW_REF }}
|
||||
token: ${{ secrets.DEVFLOW_TOKEN }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
path: devflow
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install DevFlow dependencies
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Classify issue relevance
|
||||
id: spam
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
ISSUE_REPO: ${{ needs.team_check.outputs.repo }}
|
||||
ISSUE_NUMBER: ${{ needs.team_check.outputs.issue_number }}
|
||||
run: |
|
||||
uv run python scripts/classify_issue_spam.py \
|
||||
--repo "$ISSUE_REPO" \
|
||||
--issue-number "$ISSUE_NUMBER" \
|
||||
--repo-path "${TARGET_REPO_PATH}" \
|
||||
--apply-labels
|
||||
|
||||
- name: Stop after spam gate
|
||||
if: ${{ steps.spam.outputs.decision != 'allow' }}
|
||||
shell: bash
|
||||
env:
|
||||
SPAM_DECISION: ${{ steps.spam.outputs.decision }}
|
||||
run: |
|
||||
echo "Stopping: spam gate decided: ${SPAM_DECISION}"
|
||||
exit 1
|
||||
|
||||
- name: Reproduce reported issue
|
||||
if: ${{ steps.spam.outputs.decision == 'allow' }}
|
||||
id: repro
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_COPILOT_TOKEN: ${{ secrets.GH_COPILOT_TOKEN }}
|
||||
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
ISSUE_REPO: ${{ needs.team_check.outputs.repo }}
|
||||
ISSUE_NUMBER: ${{ needs.team_check.outputs.issue_number }}
|
||||
# Model-provider settings for generated repro code. Never enter the
|
||||
# agent prompt; consumed by SDK constructors via os.environ. Azure
|
||||
# OpenAI and Foundry auth via AAD from the azure/login step above.
|
||||
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_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
|
||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
|
||||
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
|
||||
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
|
||||
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||
run: |
|
||||
uv run python scripts/trigger_issue_repro.py \
|
||||
--repo "$ISSUE_REPO" \
|
||||
--issue-number "$ISSUE_NUMBER" \
|
||||
--github-username "$GITHUB_ACTOR"
|
||||
@@ -29,7 +29,4 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout: 3600
|
||||
interval: 30
|
||||
# "Cleanup artifacts", "Agent", "Prepare", and "Upload results" are check runs
|
||||
# created by an org-level GitHub App (MSDO), not by any workflow in this repo.
|
||||
# They are outside our control and their transient failures should not block merges.
|
||||
ignored: CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results
|
||||
ignored: CodeQL,CodeQL analysis (csharp)
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""Check Python test coverage against threshold for enforced targets.
|
||||
|
||||
This script parses a Cobertura XML coverage report and enforces a minimum
|
||||
coverage threshold on specific targets. Targets can be package names
|
||||
(e.g., "packages.core.agent_framework") or individual Python file paths
|
||||
(e.g., "packages/core/agent_framework/observability.py").
|
||||
|
||||
Non-enforced targets are reported for visibility but don't block the build.
|
||||
|
||||
Usage:
|
||||
python python-check-coverage.py <coverage-xml-path> <threshold>
|
||||
|
||||
Example:
|
||||
python python-check-coverage.py python-coverage.xml 85
|
||||
"""
|
||||
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
|
||||
# =============================================================================
|
||||
# ENFORCED TARGETS CONFIGURATION
|
||||
# =============================================================================
|
||||
# Add or remove entries from this set to control which targets must meet
|
||||
# the coverage threshold. Only these targets will fail the build if below
|
||||
# threshold. Other targets are reported for visibility only.
|
||||
#
|
||||
# Target values can be:
|
||||
# - Package paths as they appear in the coverage report
|
||||
# (e.g., "packages.azure-ai.agent_framework_azure_ai")
|
||||
# - Python source file paths as they appear in the coverage report
|
||||
# (e.g., "packages/core/agent_framework/observability.py")
|
||||
# =============================================================================
|
||||
ENFORCED_TARGETS: set[str] = {
|
||||
# Packages (sorted alphabetically)
|
||||
"packages.anthropic.agent_framework_anthropic",
|
||||
"packages.azure-ai-search.agent_framework_azure_ai_search",
|
||||
"packages.core.agent_framework",
|
||||
"packages.core.agent_framework._workflows",
|
||||
"packages.foundry.agent_framework_foundry",
|
||||
"packages.openai.agent_framework_openai",
|
||||
"packages.purview.agent_framework_purview",
|
||||
# Individual files (if you want to enforce specific files instead of whole packages)
|
||||
"packages/core/agent_framework/observability.py",
|
||||
# Add more targets here as coverage improves
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageCoverage:
|
||||
"""Coverage data for a single package."""
|
||||
|
||||
name: str
|
||||
line_rate: float
|
||||
branch_rate: float
|
||||
lines_valid: int
|
||||
lines_covered: int
|
||||
branches_valid: int
|
||||
branches_covered: int
|
||||
|
||||
@property
|
||||
def line_coverage_percent(self) -> float:
|
||||
"""Return line coverage as a percentage."""
|
||||
return self.line_rate * 100
|
||||
|
||||
@property
|
||||
def branch_coverage_percent(self) -> float:
|
||||
"""Return branch coverage as a percentage."""
|
||||
return self.branch_rate * 100
|
||||
|
||||
|
||||
def normalize_coverage_path(path: str) -> str:
|
||||
"""Normalize coverage paths for reliable matching."""
|
||||
return path.replace("\\", "/").lstrip("./")
|
||||
|
||||
|
||||
def parse_coverage_xml(
|
||||
xml_path: str,
|
||||
) -> tuple[dict[str, PackageCoverage], dict[str, PackageCoverage], float, float]:
|
||||
"""Parse Cobertura XML and extract per-package coverage data.
|
||||
|
||||
Args:
|
||||
xml_path: Path to the Cobertura XML coverage report.
|
||||
|
||||
Returns:
|
||||
A tuple of (packages_dict, files_dict, overall_line_rate, overall_branch_rate).
|
||||
"""
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Get overall coverage from root element
|
||||
overall_line_rate = float(root.get("line-rate", 0))
|
||||
overall_branch_rate = float(root.get("branch-rate", 0))
|
||||
|
||||
packages: dict[str, PackageCoverage] = {}
|
||||
file_stats: dict[str, dict[str, int]] = {}
|
||||
|
||||
for package in root.findall(".//package"):
|
||||
package_path = package.get("name", "unknown")
|
||||
|
||||
line_rate = float(package.get("line-rate", 0))
|
||||
branch_rate = float(package.get("branch-rate", 0))
|
||||
|
||||
# Count lines and branches from classes within this package
|
||||
lines_valid = 0
|
||||
lines_covered = 0
|
||||
branches_valid = 0
|
||||
branches_covered = 0
|
||||
|
||||
for class_elem in package.findall(".//class"):
|
||||
file_path = normalize_coverage_path(class_elem.get("filename", ""))
|
||||
if file_path and file_path not in file_stats:
|
||||
file_stats[file_path] = {
|
||||
"lines_valid": 0,
|
||||
"lines_covered": 0,
|
||||
"branches_valid": 0,
|
||||
"branches_covered": 0,
|
||||
}
|
||||
|
||||
for line in class_elem.findall(".//line"):
|
||||
lines_valid += 1
|
||||
if int(line.get("hits", 0)) > 0:
|
||||
lines_covered += 1
|
||||
|
||||
if file_path:
|
||||
file_stats[file_path]["lines_valid"] += 1
|
||||
if int(line.get("hits", 0)) > 0:
|
||||
file_stats[file_path]["lines_covered"] += 1
|
||||
|
||||
# Branch coverage from line elements
|
||||
if line.get("branch") == "true":
|
||||
condition_coverage = line.get("condition-coverage", "")
|
||||
if condition_coverage:
|
||||
# Parse "X% (covered/total)" format
|
||||
try:
|
||||
coverage_parts = (
|
||||
condition_coverage.split("(")[1].rstrip(")").split("/")
|
||||
)
|
||||
branches_covered += int(coverage_parts[0])
|
||||
branches_valid += int(coverage_parts[1])
|
||||
if file_path:
|
||||
file_stats[file_path]["branches_covered"] += int(
|
||||
coverage_parts[0]
|
||||
)
|
||||
file_stats[file_path]["branches_valid"] += int(
|
||||
coverage_parts[1]
|
||||
)
|
||||
except (IndexError, ValueError):
|
||||
# Ignore malformed condition-coverage strings; treat this line as having no branch data.
|
||||
pass
|
||||
|
||||
# Use full package path as the key (no aggregation)
|
||||
packages[package_path] = PackageCoverage(
|
||||
name=package_path,
|
||||
line_rate=line_rate if lines_valid == 0 else lines_covered / lines_valid,
|
||||
branch_rate=branch_rate
|
||||
if branches_valid == 0
|
||||
else branches_covered / branches_valid,
|
||||
lines_valid=lines_valid,
|
||||
lines_covered=lines_covered,
|
||||
branches_valid=branches_valid,
|
||||
branches_covered=branches_covered,
|
||||
)
|
||||
|
||||
files: dict[str, PackageCoverage] = {}
|
||||
for file_path, stats in file_stats.items():
|
||||
lines_valid = stats["lines_valid"]
|
||||
lines_covered = stats["lines_covered"]
|
||||
branches_valid = stats["branches_valid"]
|
||||
branches_covered = stats["branches_covered"]
|
||||
|
||||
files[file_path] = PackageCoverage(
|
||||
name=file_path,
|
||||
line_rate=0 if lines_valid == 0 else lines_covered / lines_valid,
|
||||
branch_rate=0 if branches_valid == 0 else branches_covered / branches_valid,
|
||||
lines_valid=lines_valid,
|
||||
lines_covered=lines_covered,
|
||||
branches_valid=branches_valid,
|
||||
branches_covered=branches_covered,
|
||||
)
|
||||
|
||||
return packages, files, overall_line_rate, overall_branch_rate
|
||||
|
||||
|
||||
def format_coverage_value(coverage: float, threshold: float, is_enforced: bool) -> str:
|
||||
"""Format a coverage value with optional pass/fail indicator.
|
||||
|
||||
Args:
|
||||
coverage: Coverage percentage (0-100).
|
||||
threshold: Minimum required coverage percentage.
|
||||
is_enforced: Whether this target is enforced.
|
||||
|
||||
Returns:
|
||||
Formatted string like "85.5%" or "85.5% ✅" or "75.0% ❌".
|
||||
"""
|
||||
formatted = f"{coverage:.1f}%"
|
||||
if is_enforced:
|
||||
icon = "✅" if coverage >= threshold else "❌"
|
||||
formatted = f"{formatted} {icon}"
|
||||
return formatted
|
||||
|
||||
|
||||
def print_coverage_table(
|
||||
packages: dict[str, PackageCoverage],
|
||||
files: dict[str, PackageCoverage],
|
||||
threshold: float,
|
||||
overall_line_rate: float,
|
||||
overall_branch_rate: float,
|
||||
) -> None:
|
||||
"""Print a formatted coverage summary table.
|
||||
|
||||
Args:
|
||||
packages: Dictionary of package name to coverage data.
|
||||
files: Dictionary of file path to coverage data, used for per-file enforcement.
|
||||
threshold: Minimum required coverage percentage.
|
||||
overall_line_rate: Overall line coverage rate (0-1).
|
||||
overall_branch_rate: Overall branch coverage rate (0-1).
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("PYTHON TEST COVERAGE REPORT")
|
||||
print("=" * 80)
|
||||
|
||||
# Overall coverage
|
||||
print(f"\nOverall Line Coverage: {overall_line_rate * 100:.1f}%")
|
||||
print(f"Overall Branch Coverage: {overall_branch_rate * 100:.1f}%")
|
||||
print(f"Threshold: {threshold}%")
|
||||
|
||||
enforced_targets = {normalize_coverage_path(t) for t in ENFORCED_TARGETS}
|
||||
|
||||
# Package table
|
||||
print("\n" + "-" * 110)
|
||||
print(f"{'Package':<80} {'Lines':<15} {'Line Cov':<15}")
|
||||
print("-" * 110)
|
||||
|
||||
# Sort: enforced package targets first, then alphabetically
|
||||
sorted_packages = sorted(
|
||||
packages.values(),
|
||||
key=lambda p: (p.name not in ENFORCED_TARGETS, p.name),
|
||||
)
|
||||
|
||||
for pkg in sorted_packages:
|
||||
is_enforced = normalize_coverage_path(pkg.name) in enforced_targets
|
||||
enforced_marker = "[ENFORCED] " if is_enforced else ""
|
||||
line_cov = format_coverage_value(
|
||||
pkg.line_coverage_percent, threshold, is_enforced
|
||||
)
|
||||
lines_info = f"{pkg.lines_covered}/{pkg.lines_valid}"
|
||||
package_label = f"{enforced_marker}{pkg.name}"
|
||||
|
||||
print(f"{package_label:<80} {lines_info:<15} {line_cov:<15}")
|
||||
|
||||
print("-" * 110)
|
||||
|
||||
# Enforced file/model entries (if configured)
|
||||
enforced_files = [
|
||||
files[target]
|
||||
for target in sorted(enforced_targets)
|
||||
if target in files and target.endswith(".py")
|
||||
]
|
||||
|
||||
if enforced_files:
|
||||
print("\nEnforced Files/Models")
|
||||
print("-" * 110)
|
||||
print(f"{'File':<80} {'Lines':<15} {'Line Cov':<15}")
|
||||
print("-" * 110)
|
||||
|
||||
for file_cov in enforced_files:
|
||||
line_cov = format_coverage_value(
|
||||
file_cov.line_coverage_percent, threshold, True
|
||||
)
|
||||
lines_info = f"{file_cov.lines_covered}/{file_cov.lines_valid}"
|
||||
print(f"[ENFORCED] {file_cov.name:<69} {lines_info:<15} {line_cov:<15}")
|
||||
|
||||
print("-" * 110)
|
||||
|
||||
|
||||
def check_coverage(xml_path: str, threshold: float) -> bool:
|
||||
"""Check if all enforced targets meet the coverage threshold.
|
||||
|
||||
Args:
|
||||
xml_path: Path to the Cobertura XML coverage report.
|
||||
threshold: Minimum required coverage percentage.
|
||||
|
||||
Returns:
|
||||
True if all enforced targets pass, False otherwise.
|
||||
"""
|
||||
packages, files, overall_line_rate, overall_branch_rate = parse_coverage_xml(
|
||||
xml_path
|
||||
)
|
||||
|
||||
print_coverage_table(
|
||||
packages, files, threshold, overall_line_rate, overall_branch_rate
|
||||
)
|
||||
|
||||
# Check enforced targets
|
||||
failed_targets: list[str] = []
|
||||
missing_targets: list[str] = []
|
||||
|
||||
for target_name in ENFORCED_TARGETS:
|
||||
normalized_target = normalize_coverage_path(target_name)
|
||||
package_alias = normalized_target.replace("/", ".")
|
||||
|
||||
target_coverage = None
|
||||
if target_name in packages:
|
||||
target_coverage = packages[target_name]
|
||||
elif normalized_target in files:
|
||||
target_coverage = files[normalized_target]
|
||||
elif package_alias in packages:
|
||||
target_coverage = packages[package_alias]
|
||||
|
||||
if target_coverage is None:
|
||||
missing_targets.append(target_name)
|
||||
continue
|
||||
|
||||
if target_coverage.line_coverage_percent < threshold:
|
||||
failed_targets.append(
|
||||
f"{target_name} ({target_coverage.line_coverage_percent:.1f}%)"
|
||||
)
|
||||
|
||||
# Report results
|
||||
if missing_targets:
|
||||
print(
|
||||
f"\n❌ FAILED: Enforced targets not found in coverage report: {', '.join(missing_targets)}"
|
||||
)
|
||||
return False
|
||||
|
||||
if failed_targets:
|
||||
print(
|
||||
f"\n❌ FAILED: The following enforced targets are below {threshold}% coverage threshold:"
|
||||
)
|
||||
for target in failed_targets:
|
||||
print(f" - {target}")
|
||||
print("\nTo fix: Add more tests to improve coverage for the failing targets.")
|
||||
return False
|
||||
|
||||
if ENFORCED_TARGETS:
|
||||
found_enforced = [
|
||||
target
|
||||
for target in ENFORCED_TARGETS
|
||||
if target in packages or normalize_coverage_path(target) in files
|
||||
]
|
||||
if found_enforced:
|
||||
print(
|
||||
f"\n✅ PASSED: All enforced targets meet the {threshold}% coverage threshold."
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point.
|
||||
|
||||
Returns:
|
||||
Exit code: 0 for success, 1 for failure.
|
||||
"""
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <coverage-xml-path> <threshold>")
|
||||
print(f"Example: {sys.argv[0]} python-coverage.xml 85")
|
||||
return 1
|
||||
|
||||
xml_path = sys.argv[1]
|
||||
try:
|
||||
threshold = float(sys.argv[2])
|
||||
except ValueError:
|
||||
print(f"Error: Invalid threshold value: {sys.argv[2]}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
success = check_coverage(xml_path, threshold)
|
||||
return 0 if success else 1
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Coverage file not found: {xml_path}")
|
||||
return 1
|
||||
except ET.ParseError as e:
|
||||
print(f"Error: Failed to parse coverage XML: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -12,13 +12,13 @@ env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
jobs:
|
||||
pre-commit-hooks:
|
||||
name: Pre-commit Hooks
|
||||
pre-commit:
|
||||
name: Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
python-version: ["3.10", "3.14"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
@@ -37,105 +37,17 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/prek
|
||||
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: j178/prek-action@v1
|
||||
name: Run Pre-commit Hooks (excluding poe-check)
|
||||
env:
|
||||
SKIP: poe-check
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
name: Run Pre-Commit Hooks
|
||||
with:
|
||||
extra-args: --cd python --all-files
|
||||
|
||||
package-checks:
|
||||
name: Package Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run syntax and pyright across packages
|
||||
run: uv run poe check-packages
|
||||
|
||||
samples-markdown:
|
||||
name: Samples & Markdown
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run samples checks
|
||||
run: uv run poe check -S
|
||||
- name: Run markdown code lint
|
||||
run: uv run poe markdown-code-lint
|
||||
|
||||
mypy:
|
||||
name: Mypy Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
extra_args: --config python/.pre-commit-config.yaml --all-files
|
||||
- name: Run Mypy
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
|
||||
run: uv run python scripts/workspace_poe_tasks.py ci-mypy
|
||||
run: uv run poe ci-mypy
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
# Probe the highest allowed dependency versions, then open issues/PRs from the passing updates.
|
||||
name: Python - Dependency Range Validation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
jobs:
|
||||
dependency-range-validation:
|
||||
name: Dependency Range Validation
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# For now only run 3.13, if we do encounter situations where there are mismatches between packages and python versions (other then 3.10 and 3.14 which are known to not be able to install everything)
|
||||
# then we will have to reevaluate.
|
||||
UV_PYTHON: "3.13"
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
- name: Run dependency range validation
|
||||
id: validate_ranges
|
||||
# Keep workflow running so we can still publish diagnostics from this run.
|
||||
continue-on-error: true
|
||||
run: uv run poe validate-dependency-bounds-project --mode upper --package "*"
|
||||
working-directory: ./python
|
||||
|
||||
- name: Upload dependency range report
|
||||
# Always publish the report so failures are inspectable even when validation fails.
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dependency-range-results
|
||||
path: python/scripts/dependencies/dependency-range-results.json
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Create issues for failed dependency candidates
|
||||
# Always process the report so failed candidates create actionable tracking issues.
|
||||
if: always()
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs")
|
||||
const reportPath = "python/scripts/dependencies/dependency-range-results.json"
|
||||
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
core.warning(`No dependency range report found at ${reportPath}`)
|
||||
return
|
||||
}
|
||||
|
||||
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
||||
const dependencyFailures = []
|
||||
|
||||
for (const packageResult of report.packages ?? []) {
|
||||
for (const dependency of packageResult.dependencies ?? []) {
|
||||
const candidateVersions = new Set(dependency.candidate_versions ?? [])
|
||||
const failedAttempts = (dependency.attempts ?? []).filter(
|
||||
(attempt) => attempt.status === "failed" && candidateVersions.has(attempt.trial_upper)
|
||||
)
|
||||
if (!failedAttempts.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const failuresByVersion = new Map()
|
||||
for (const attempt of failedAttempts) {
|
||||
const version = attempt.trial_upper || "unknown"
|
||||
if (!failuresByVersion.has(version)) {
|
||||
failuresByVersion.set(version, attempt.error || "No error output captured.")
|
||||
}
|
||||
}
|
||||
|
||||
dependencyFailures.push({
|
||||
packageName: packageResult.package_name,
|
||||
projectPath: packageResult.project_path,
|
||||
dependencyName: dependency.name,
|
||||
originalRequirements: dependency.original_requirements ?? [],
|
||||
finalRequirements: dependency.final_requirements ?? [],
|
||||
failedVersions: [...failuresByVersion.entries()].map(([version, error]) => ({ version, error })),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!dependencyFailures.length) {
|
||||
core.info("No failing dependency candidates found.")
|
||||
return
|
||||
}
|
||||
|
||||
const owner = context.repo.owner
|
||||
const repo = context.repo.repo
|
||||
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
})
|
||||
const openIssueTitles = new Set(
|
||||
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
|
||||
)
|
||||
|
||||
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
|
||||
|
||||
for (const failure of dependencyFailures) {
|
||||
const title = `Dependency validation failed: ${failure.dependencyName} (${failure.packageName})`
|
||||
if (openIssueTitles.has(title)) {
|
||||
core.info(`Issue already exists: ${title}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const visibleFailures = failure.failedVersions.slice(0, 5)
|
||||
const omittedCount = failure.failedVersions.length - visibleFailures.length
|
||||
const failureDetails = visibleFailures
|
||||
.map(
|
||||
(entry) =>
|
||||
`- \`${entry.version}\`\n\n\`\`\`\n${formatError(entry.error).slice(0, 3500)}\n\`\`\``
|
||||
)
|
||||
.join("\n\n")
|
||||
|
||||
const body = [
|
||||
"Automated dependency range validation found candidate versions that failed checks.",
|
||||
"",
|
||||
`- Package: \`${failure.packageName}\``,
|
||||
`- Project path: \`${failure.projectPath}\``,
|
||||
`- Dependency: \`${failure.dependencyName}\``,
|
||||
`- Original requirements: ${
|
||||
failure.originalRequirements.length
|
||||
? failure.originalRequirements.map((value) => `\`${value}\``).join(", ")
|
||||
: "_none_"
|
||||
}`,
|
||||
`- Final requirements after run: ${
|
||||
failure.finalRequirements.length
|
||||
? failure.finalRequirements.map((value) => `\`${value}\``).join(", ")
|
||||
: "_none_"
|
||||
}`,
|
||||
"",
|
||||
"### Failed versions and errors",
|
||||
failureDetails,
|
||||
omittedCount > 0 ? `\n_Additional failed versions omitted: ${omittedCount}_` : "",
|
||||
"",
|
||||
`Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
|
||||
].join("\n")
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
})
|
||||
openIssueTitles.add(title)
|
||||
core.info(`Created issue: ${title}`)
|
||||
}
|
||||
|
||||
- name: Refresh lockfile
|
||||
# Only refresh lockfile after a clean validation to avoid committing known-bad ranges.
|
||||
if: steps.validate_ranges.outcome == 'success'
|
||||
run: uv lock --upgrade
|
||||
working-directory: ./python
|
||||
|
||||
- name: Commit and push dependency updates
|
||||
id: commit_updates
|
||||
if: steps.validate_ranges.outcome == 'success'
|
||||
run: |
|
||||
BRANCH="automation/python-dependency-range-updates"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -B "${BRANCH}"
|
||||
|
||||
git add python/packages/*/pyproject.toml python/uv.lock
|
||||
if git diff --cached --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No dependency updates to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore: update dependency ranges"
|
||||
git push --force-with-lease --set-upstream origin "${BRANCH}"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create or update pull request with GitHub CLI
|
||||
# Only open/update PRs for validated updates to keep automation branches trustworthy.
|
||||
if: steps.validate_ranges.outcome == 'success' && steps.commit_updates.outputs.has_changes == 'true'
|
||||
run: |
|
||||
BRANCH="automation/python-dependency-range-updates"
|
||||
PR_TITLE="Python: chore: update dependency ranges"
|
||||
PR_BODY_FILE="$(mktemp)"
|
||||
|
||||
cat > "${PR_BODY_FILE}" <<'EOF'
|
||||
This PR was generated by the dependency range validation workflow.
|
||||
|
||||
- Ran `uv run poe validate-dependency-bounds-project --mode upper --package "*"`
|
||||
- Updated package dependency bounds
|
||||
- Refreshed `python/uv.lock` with `uv lock --upgrade`
|
||||
EOF
|
||||
|
||||
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
|
||||
if [ -n "${PR_NUMBER}" ]; then
|
||||
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
|
||||
else
|
||||
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
|
||||
fi
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Python - Dev Dependency Upgrade
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
jobs:
|
||||
upgrade-dev-dependencies:
|
||||
name: Upgrade Dev Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
UV_PYTHON: "3.13"
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
- name: Upgrade dev dependencies and validate workspace
|
||||
run: uv run poe upgrade-dev-dependencies
|
||||
working-directory: ./python
|
||||
|
||||
- name: Commit and push dev dependency updates
|
||||
id: commit_updates
|
||||
run: |
|
||||
BRANCH="automation/python-dev-dependency-updates"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -B "${BRANCH}"
|
||||
|
||||
git add python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
|
||||
if git diff --cached --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No dev dependency updates to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -F- <<'EOF'
|
||||
Python: chore: upgrade dev dependencies
|
||||
EOF
|
||||
git push --force-with-lease --set-upstream origin "${BRANCH}"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create or update pull request with GitHub CLI
|
||||
if: steps.commit_updates.outputs.has_changes == 'true'
|
||||
run: |
|
||||
BRANCH="automation/python-dev-dependency-updates"
|
||||
PR_TITLE="Python: chore: upgrade dev dependencies"
|
||||
PR_BODY_FILE="$(mktemp)"
|
||||
|
||||
cat > "${PR_BODY_FILE}" <<'EOF'
|
||||
### Motivation and Context
|
||||
|
||||
This automated update refreshes Python dev dependency pins across the workspace and reruns the repo validation gates before opening a pull request.
|
||||
|
||||
### Description
|
||||
|
||||
- Ran `uv run poe upgrade-dev-dependencies`
|
||||
- Refreshed dev dependency pins in workspace `pyproject.toml` files
|
||||
- Refreshed `python/uv.lock` with `uv lock --upgrade`
|
||||
- Reinstalled from the frozen lockfile and reran `check`, `typing`, and `test`
|
||||
|
||||
### Contribution Checklist
|
||||
|
||||
- [x] The code builds clean without any errors or warnings
|
||||
- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
|
||||
- [x] All unit tests pass, and I have added new tests where possible
|
||||
- [ ] **Is this a breaking change?** If yes, add "[BREAKING]" prefix to the title of the PR.
|
||||
EOF
|
||||
|
||||
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
|
||||
if [ -n "${PR_NUMBER}" ]; then
|
||||
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
|
||||
else
|
||||
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
|
||||
fi
|
||||
@@ -1,530 +0,0 @@
|
||||
#
|
||||
# Dedicated Python integration tests workflow, called from the manual integration test orchestrator.
|
||||
# Runs all tests (unit + integration) split into parallel jobs by provider.
|
||||
#
|
||||
# NOTE: This workflow and python-merge-tests.yml share the same set of parallel
|
||||
# test jobs. Keep them in sync — when adding, removing, or modifying a job here,
|
||||
# apply the same change to python-merge-tests.yml.
|
||||
#
|
||||
|
||||
name: python-integration-tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
checkout-ref:
|
||||
description: "Git ref to checkout (e.g., refs/pull/123/head)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
UV_PYTHON: "3.13"
|
||||
|
||||
jobs:
|
||||
# Unit tests: all non-integration tests across all packages
|
||||
python-tests-unit:
|
||||
name: Python Integration Tests - Unit
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Test with pytest (unit tests only)
|
||||
run: >
|
||||
uv run poe test -A
|
||||
-m "not integration"
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
|
||||
# OpenAI integration tests
|
||||
python-tests-openai:
|
||||
name: Python Integration Tests - OpenAI
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
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 }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Test with pytest (OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/openai/tests
|
||||
-m "integration and not azure"
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-openai
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Azure OpenAI integration tests
|
||||
python-tests-azure-openai:
|
||||
name: Python Integration Tests - Azure OpenAI
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest (Azure OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
|
||||
packages/openai/tests/openai/test_openai_chat_client_azure.py
|
||||
packages/openai/tests/openai/test_openai_embedding_client_azure.py
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-azure-openai
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Misc integration tests (Anthropic, Hyperlight, Ollama, MCP)
|
||||
python-tests-misc-integration:
|
||||
name: Python Integration Tests - Misc
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Start local MCP server
|
||||
id: local-mcp
|
||||
uses: ./.github/actions/setup-local-mcp-server
|
||||
with:
|
||||
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, Ollama, MCP integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/anthropic/tests
|
||||
packages/hyperlight/tests
|
||||
packages/ollama/tests
|
||||
packages/core/tests/core/test_mcp.py
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 30
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-misc
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
- name: Stop local MCP server
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
server_pid="${{ steps.local-mcp.outputs.pid }}"
|
||||
if [[ -z "$server_pid" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" 2>/dev/null || true
|
||||
for _ in $(seq 1 10); do
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
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
|
||||
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@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@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=360 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@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
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
|
||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
|
||||
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
|
||||
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
|
||||
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
|
||||
FOUNDRY_IMAGE_EMBEDDING_MODEL: ${{ vars.FOUNDRY_IMAGE_EMBEDDING_MODEL || '' }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 15
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/foundry/tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-foundry
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Foundry Hosting integration tests
|
||||
python-tests-foundry-hosting:
|
||||
name: Python Integration Tests - Foundry Hosting
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest (Foundry Hosting integration)
|
||||
timeout-minutes: 15
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/foundry_hosting/tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-foundry-hosting
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Azure Cosmos integration tests
|
||||
python-tests-cosmos:
|
||||
name: Python Integration Tests - Cosmos
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
services:
|
||||
cosmosdb:
|
||||
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
|
||||
ports:
|
||||
- 8081:8081
|
||||
env:
|
||||
AZURE_COSMOS_ENDPOINT: "http://localhost:8081/"
|
||||
# Static Azure Cosmos DB emulator key (documented): https://learn.microsoft.com/en-us/azure/cosmos-db/emulator
|
||||
AZURE_COSMOS_KEY: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
AZURE_COSMOS_DATABASE_NAME: "agent-framework-cosmos-it-db"
|
||||
AZURE_COSMOS_CONTAINER_NAME: "agent-framework-cosmos-it-container"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Wait for Cosmos DB emulator
|
||||
run: |
|
||||
for i in {1..60}; do
|
||||
if curl --silent --show-error http://localhost:8081/ > /dev/null; then
|
||||
echo "Cosmos DB emulator is ready."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Cosmos DB emulator did not become ready in time." >&2
|
||||
exit 1
|
||||
- name: Test with pytest (Cosmos integration)
|
||||
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-cosmos
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Flaky test trend report (aggregates per-job JUnit XML results)
|
||||
python-flaky-test-report:
|
||||
name: Flaky Test Report
|
||||
if: >
|
||||
always() &&
|
||||
(contains(join(needs.*.result, ','), 'success') ||
|
||||
contains(join(needs.*.result, ','), 'failure'))
|
||||
needs:
|
||||
[
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Download all test results from current run
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore flaky report history cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: python/flaky-report-history.json
|
||||
key: flaky-report-history-integration-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
flaky-report-history-integration-
|
||||
- name: Generate trend report
|
||||
run: >
|
||||
uv run python scripts/flaky_report/aggregate.py
|
||||
../test-results/
|
||||
flaky-report-history.json
|
||||
flaky-test-report.md
|
||||
- name: Post to Job Summary
|
||||
if: always()
|
||||
run: cat flaky-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save flaky report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: python/flaky-report-history.json
|
||||
key: flaky-report-history-integration-${{ github.run_id }}
|
||||
- name: Upload unified trend report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: flaky-test-report
|
||||
path: |
|
||||
python/flaky-test-report.md
|
||||
python/flaky-report-history.json
|
||||
|
||||
python-integration-tests-check:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
python-tests-unit,
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos
|
||||
]
|
||||
steps:
|
||||
- name: Fail workflow if tests failed
|
||||
if: contains(join(needs.*.result, ','), 'failure')
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Failed!')
|
||||
|
||||
- name: Fail workflow if tests cancelled
|
||||
if: contains(join(needs.*.result, ','), 'cancelled')
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Cancelled!')
|
||||
@@ -67,7 +67,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
@@ -76,9 +75,6 @@ jobs:
|
||||
- name: Run lab tests
|
||||
run: cd packages/lab && uv run poe test
|
||||
|
||||
- name: Run resource-intensive lab tests
|
||||
run: cd packages/lab && uv run pytest -m "resource_intensive and not integration" --junitxml=test-results-resource-intensive.xml
|
||||
|
||||
- name: Run lab lint
|
||||
run: cd packages/lab && uv run poe lint
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
name: Python - Merge - Tests
|
||||
#
|
||||
# NOTE: This workflow and python-integration-tests.yml share the same set of
|
||||
# parallel test jobs. Keep them in sync — when adding, removing, or modifying a
|
||||
# job here, apply the same change to python-integration-tests.yml.
|
||||
#
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -15,13 +10,13 @@ on:
|
||||
- cron: "0 0 * * *" # Run at midnight UTC daily
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
UV_PYTHON: "3.13"
|
||||
RUN_INTEGRATION_TESTS: "true"
|
||||
RUN_SAMPLES_TESTS: ${{ vars.RUN_SAMPLES_TESTS }}
|
||||
|
||||
jobs:
|
||||
@@ -31,15 +26,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python }}
|
||||
coreChanged: ${{ steps.filter.outputs.core }}
|
||||
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 }}
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
@@ -48,43 +35,6 @@ jobs:
|
||||
filters: |
|
||||
python:
|
||||
- 'python/**'
|
||||
- '.github/actions/setup-local-mcp-server/**'
|
||||
- '.github/workflows/python-merge-tests.yml'
|
||||
- '.github/workflows/python-integration-tests.yml'
|
||||
core:
|
||||
- 'python/packages/core/agent_framework/_*.py'
|
||||
- 'python/packages/core/agent_framework/_workflows/**'
|
||||
- 'python/packages/core/agent_framework/exceptions.py'
|
||||
- 'python/packages/core/agent_framework/observability.py'
|
||||
openai:
|
||||
- 'python/packages/core/agent_framework/openai/**'
|
||||
- 'python/packages/openai/**'
|
||||
- 'python/samples/**/providers/openai/**'
|
||||
azure:
|
||||
- 'python/packages/openai/**'
|
||||
- 'python/packages/core/agent_framework/azure/**'
|
||||
- 'python/samples/**/providers/azure/**'
|
||||
misc:
|
||||
- 'python/packages/anthropic/**'
|
||||
- 'python/packages/hyperlight/**'
|
||||
- 'python/packages/ollama/**'
|
||||
- 'python/packages/core/agent_framework/_mcp.py'
|
||||
- 'python/packages/core/tests/core/test_mcp.py'
|
||||
- 'python/scripts/local_mcp_streamable_http_server.py'
|
||||
- '.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/**'
|
||||
- 'python/samples/02-agents/embeddings/foundry_embeddings.py'
|
||||
foundry_hosting:
|
||||
- 'python/packages/foundry_hosting/**'
|
||||
cosmos:
|
||||
- 'python/packages/azure-cosmos/**'
|
||||
# run only if 'python' files were changed
|
||||
- name: python tests
|
||||
if: steps.filter.outputs.python == 'true'
|
||||
@@ -93,287 +43,34 @@ jobs:
|
||||
- name: not python tests
|
||||
if: steps.filter.outputs.python != 'true'
|
||||
run: echo "NOT python file"
|
||||
# Unit tests: always run all non-integration tests across all packages
|
||||
python-tests-unit:
|
||||
name: Python Tests - Unit
|
||||
python-tests-core:
|
||||
name: Python Tests - Core
|
||||
needs: paths-filter
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
needs.paths-filter.outputs.pythonChanges == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Test with pytest (unit tests only)
|
||||
run: >
|
||||
uv run poe test -A
|
||||
-m "not integration"
|
||||
--timeout=120 --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@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Unit test results
|
||||
|
||||
# OpenAI integration tests
|
||||
python-tests-openai:
|
||||
name: Python Tests - OpenAI 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.openaiChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
if: github.event_name != 'pull_request' && needs.paths-filter.outputs.pythonChanges == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
os: [ubuntu-latest]
|
||||
environment: ["integration"]
|
||||
env:
|
||||
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 }}
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Test with pytest (OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/openai/tests
|
||||
-m "integration and not azure"
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
working-directory: ./python
|
||||
- name: Test OpenAI samples
|
||||
timeout-minutes: 10
|
||||
if: env.RUN_SAMPLES_TESTS == 'true'
|
||||
run: uv run pytest tests/samples/ -m "openai"
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: OpenAI integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-openai
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Azure OpenAI integration tests
|
||||
python-tests-azure-openai:
|
||||
name: Python Tests - Azure OpenAI 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.azureChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest (Azure OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
|
||||
packages/openai/tests/openai/test_openai_chat_client_azure.py
|
||||
packages/openai/tests/openai/test_openai_embedding_client_azure.py
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
working-directory: ./python
|
||||
- name: Test Azure samples
|
||||
timeout-minutes: 10
|
||||
if: env.RUN_SAMPLES_TESTS == 'true'
|
||||
run: uv run pytest tests/samples/ -m "azure"
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Azure OpenAI integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-azure-openai
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Misc integration tests (Anthropic, Ollama, MCP)
|
||||
python-tests-misc-integration:
|
||||
name: Python Tests - Misc 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.miscChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Start local MCP server
|
||||
id: local-mcp
|
||||
uses: ./.github/actions/setup-local-mcp-server
|
||||
with:
|
||||
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, Ollama, MCP integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/anthropic/tests
|
||||
packages/hyperlight/tests
|
||||
packages/ollama/tests
|
||||
packages/core/tests/core/test_mcp.py
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 30
|
||||
--junitxml=pytest.xml
|
||||
working-directory: ./python
|
||||
- name: Stop local MCP server
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
server_pid="${{ steps.local-mcp.outputs.pid }}"
|
||||
if [[ -z "$server_pid" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" 2>/dev/null || true
|
||||
for _ in $(seq 1 10); do
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Misc integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-misc
|
||||
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 }}
|
||||
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
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 }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
# For Azure Functions integration tests
|
||||
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
|
||||
@@ -383,8 +80,11 @@ jobs:
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@v2
|
||||
@@ -395,55 +95,41 @@ jobs:
|
||||
- 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=360 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 600 --retries 3 --retry-delay 10
|
||||
working-directory: ./python
|
||||
- name: Test core samples
|
||||
timeout-minutes: 10
|
||||
if: env.RUN_SAMPLES_TESTS == 'true'
|
||||
run: uv run pytest tests/samples/ -m "openai" -m "azure"
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
path: ./python/**.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@v7
|
||||
with:
|
||||
name: test-results-functions
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
title: Test results
|
||||
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
python-tests-azure-ai:
|
||||
name: Python Tests - Azure AI
|
||||
needs: paths-filter
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
needs.paths-filter.outputs.pythonChanges == 'true' &&
|
||||
(github.event_name != 'merge_group' ||
|
||||
needs.paths-filter.outputs.foundryChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
if: github.event_name != 'pull_request' && needs.paths-filter.outputs.pythonChanges == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: ${{ matrix.environment }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
os: [ubuntu-latest]
|
||||
environment: ["integration"]
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
|
||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
|
||||
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
|
||||
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
|
||||
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
|
||||
FOUNDRY_IMAGE_EMBEDDING_MODEL: ${{ vars.FOUNDRY_IMAGE_EMBEDDING_MODEL || '' }}
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -454,8 +140,11 @@ jobs:
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@v2
|
||||
@@ -464,240 +153,36 @@ jobs:
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 15
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/foundry/tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
timeout-minutes: 10
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
|
||||
working-directory: ./python
|
||||
- name: Test Azure AI samples
|
||||
timeout-minutes: 10
|
||||
if: env.RUN_SAMPLES_TESTS == 'true'
|
||||
run: uv run pytest tests/samples/ -m "azure-ai"
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
path: ./python/**.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-foundry
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Foundry Hosting integration tests
|
||||
python-tests-foundry-hosting:
|
||||
name: Python Tests - Foundry Hosting 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.foundryHostingChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
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@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@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest (Foundry Hosting integration)
|
||||
timeout-minutes: 15
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/foundry_hosting/tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --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@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Foundry Hosting integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-foundry-hosting
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# TODO: Add python-tests-lab
|
||||
|
||||
# Azure Cosmos integration tests
|
||||
python-tests-cosmos:
|
||||
name: Python Tests - Cosmos 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.cosmosChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
services:
|
||||
cosmosdb:
|
||||
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
|
||||
ports:
|
||||
- 8081:8081
|
||||
env:
|
||||
AZURE_COSMOS_ENDPOINT: "http://localhost:8081/"
|
||||
# Static Azure Cosmos DB emulator key (documented): https://learn.microsoft.com/en-us/azure/cosmos-db/emulator
|
||||
AZURE_COSMOS_KEY: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
||||
AZURE_COSMOS_DATABASE_NAME: "agent-framework-cosmos-it-db"
|
||||
AZURE_COSMOS_CONTAINER_NAME: "agent-framework-cosmos-it-container"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@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: Wait for Cosmos DB emulator
|
||||
run: |
|
||||
for i in {1..60}; do
|
||||
if curl --silent --show-error http://localhost:8081/ > /dev/null; then
|
||||
echo "Cosmos DB emulator is ready."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Cosmos DB emulator did not become ready in time." >&2
|
||||
exit 1
|
||||
- name: Test with pytest (Cosmos integration)
|
||||
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: Cosmos integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-cosmos
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Flaky test trend report (aggregates per-job JUnit XML results)
|
||||
python-flaky-test-report:
|
||||
name: Flaky Test Report
|
||||
if: >
|
||||
always() &&
|
||||
(contains(join(needs.*.result, ','), 'success') ||
|
||||
contains(join(needs.*.result, ','), 'failure'))
|
||||
needs:
|
||||
[
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Download all test results from current run
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore flaky report history cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: python/flaky-report-history.json
|
||||
key: flaky-report-history-merge-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
flaky-report-history-merge-
|
||||
- name: Generate trend report
|
||||
run: >
|
||||
uv run python scripts/flaky_report/aggregate.py
|
||||
../test-results/
|
||||
flaky-report-history.json
|
||||
flaky-test-report.md
|
||||
- name: Post to Job Summary
|
||||
if: always()
|
||||
run: cat flaky-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save flaky report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: python/flaky-report-history.json
|
||||
key: flaky-report-history-merge-${{ github.run_id }}
|
||||
- name: Upload unified trend report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: flaky-test-report
|
||||
path: |
|
||||
python/flaky-test-report.md
|
||||
python/flaky-report-history.json
|
||||
|
||||
python-integration-tests-check:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
python-tests-unit,
|
||||
python-tests-openai,
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
python-tests-core,
|
||||
python-tests-azure-ai
|
||||
]
|
||||
steps:
|
||||
|
||||
- name: Fail workflow if tests failed
|
||||
id: check_tests_failed
|
||||
if: contains(join(needs.*.result, ','), 'failure')
|
||||
|
||||
@@ -1,732 +0,0 @@
|
||||
name: Python - Sample Validation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # Run at midnight UTC daily
|
||||
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
# GitHub Copilot configuration
|
||||
GITHUB_COPILOT_MODEL: claude-opus-4.6
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
validate-01-get-started:
|
||||
name: Validate 01-get-started
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Required configuration for get-started samples
|
||||
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@v6
|
||||
|
||||
- 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 01-get-started --save-report --report-name 01-get-started
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-01-get-started
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents:
|
||||
name: Validate 02-agents
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Foundry configuration
|
||||
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 }}
|
||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
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__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__RESPONSESMODELID }}
|
||||
# Observability
|
||||
ENABLE_INSTRUMENTATION: "true"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
||||
echo "AZURE_OPENAI_CHAT_COMPLETION_MODEL=$AZURE_OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
||||
echo "AZURE_OPENAI_CHAT_MODEL=$AZURE_OPENAI_CHAT_MODEL" >> .env
|
||||
echo "AZURE_OPENAI_EMBEDDING_MODEL=$AZURE_OPENAI_EMBEDDING_MODEL" >> .env
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
||||
echo "GITHUB_PAT=$GITHUB_PAT" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
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@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-openai:
|
||||
name: Validate 02-agents/providers/openai
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
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@v6
|
||||
|
||||
- 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 "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_MODEL=$OPENAI_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-openai
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-azure:
|
||||
name: Validate 02-agents/providers/azure
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
||||
echo "AZURE_OPENAI_API_VERSION=$AZURE_OPENAI_API_VERSION" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-azure
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-anthropic:
|
||||
name: Validate 02-agents/providers/anthropic
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env
|
||||
echo "ANTHROPIC_CHAT_MODEL=$ANTHROPIC_CHAT_MODEL" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-anthropic
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-github-copilot:
|
||||
name: Validate 02-agents/providers/github_copilot
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-github-copilot
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-amazon:
|
||||
name: Validate 02-agents/providers/amazon
|
||||
if: false # Temporarily disabled - requires AWS credentials
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
BEDROCK_CHAT_MODEL: ${{ vars.BEDROCK__CHATMODELID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-amazon
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-ollama:
|
||||
name: Validate 02-agents/providers/ollama
|
||||
if: false # Temporarily disabled - requires local Ollama server
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
OLLAMA_MODEL: ${{ vars.OLLAMA__MODEL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-ollama
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-foundry:
|
||||
name: Validate 02-agents/providers/foundry
|
||||
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 || 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@v6
|
||||
|
||||
- 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_AGENT_NAME=$FOUNDRY_AGENT_NAME" >> .env
|
||||
echo "FOUNDRY_AGENT_VERSION=$FOUNDRY_AGENT_VERSION" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-foundry
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-copilotstudio:
|
||||
name: Validate 02-agents/providers/copilotstudio
|
||||
if: false # Temporarily disabled - requires Copilot Studio setup
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
||||
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
||||
COPILOTSTUDIOAGENT__TENANTID: ${{ secrets.COPILOTSTUDIOAGENT__TENANTID }}
|
||||
COPILOTSTUDIOAGENT__AGENTAPPID: ${{ secrets.COPILOTSTUDIOAGENT__AGENTAPPID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__AGENTAPPID=$COPILOTSTUDIOAGENT__AGENTAPPID" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-copilotstudio
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-custom:
|
||||
name: Validate 02-agents/providers/custom
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-custom
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-03-workflows:
|
||||
name: Validate 03-workflows
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
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@v6
|
||||
|
||||
- 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 03-workflows --save-report --report-name 03-workflows
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-03-workflows
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
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 || 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@v6
|
||||
|
||||
- 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
|
||||
run: |
|
||||
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@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-04-hosting
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-05-end-to-end:
|
||||
name: Validate 05-end-to-end
|
||||
if: false # Temporarily disabled because of sample complexity
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
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 }}
|
||||
# Azure AI Search (for evaluation samples)
|
||||
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
|
||||
AZURE_SEARCH_API_KEY: ${{ secrets.AZURE_SEARCH_API_KEY }}
|
||||
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
|
||||
# Evaluation sample
|
||||
FOUNDRY_MODEL_WORKFLOW: ${{ vars.FOUNDRY_MODEL_WORKFLOW || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
FOUNDRY_MODEL_EVAL: ${{ vars.FOUNDRY_MODEL_EVAL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-05-end-to-end
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-autogen-migration:
|
||||
name: Validate autogen-migration
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
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__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@v6
|
||||
|
||||
- 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 "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
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@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-autogen-migration
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-semantic-kernel-migration:
|
||||
name: Validate semantic-kernel-migration
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
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__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__CHATMODELID }}
|
||||
# Copilot Studio
|
||||
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
||||
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
||||
COPILOTSTUDIOAGENT__TENANTID: ${{ secrets.COPILOTSTUDIOAGENT__TENANTID }}
|
||||
COPILOTSTUDIOAGENT__AGENTAPPID: ${{ secrets.COPILOTSTUDIOAGENT__AGENTAPPID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__AGENTAPPID=$COPILOTSTUDIOAGENT__AGENTAPPID" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-semantic-kernel-migration
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
aggregate-results:
|
||||
name: Aggregate Results
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- validate-01-get-started
|
||||
- validate-02-agents
|
||||
- validate-02-agents-openai
|
||||
- validate-02-agents-azure
|
||||
- validate-02-agents-anthropic
|
||||
- validate-02-agents-github-copilot
|
||||
- validate-02-agents-amazon
|
||||
- validate-02-agents-ollama
|
||||
- validate-02-agents-foundry
|
||||
- validate-02-agents-copilotstudio
|
||||
- validate-02-agents-custom
|
||||
- validate-03-workflows
|
||||
- validate-04-hosting
|
||||
- validate-05-end-to-end
|
||||
- validate-autogen-migration
|
||||
- validate-semantic-kernel-migration
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download all validation reports
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: validation-report-*
|
||||
path: reports/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Restore validation history
|
||||
id: cache-restore
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
validation-history-
|
||||
|
||||
- name: Aggregate results and generate trend report
|
||||
run: |
|
||||
python3 python/scripts/sample_validation/aggregate.py \
|
||||
reports/ \
|
||||
validation-history/history.json \
|
||||
trend-report.md
|
||||
|
||||
- name: Write trend report to job summary
|
||||
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save validation history
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
|
||||
- name: Upload trend report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-trend-report
|
||||
path: trend-report.md
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download coverage report
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@v1.6.0
|
||||
uses: MishaKav/pytest-coverage-comment@v1.2.0
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
|
||||
@@ -9,8 +9,6 @@ on:
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
# Coverage threshold percentage for enforced modules
|
||||
COVERAGE_THRESHOLD: 85
|
||||
|
||||
jobs:
|
||||
python-tests-coverage:
|
||||
@@ -20,7 +18,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
UV_PYTHON: "3.10"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
# Save the PR number to a file since the workflow_run event
|
||||
@@ -32,17 +30,15 @@ jobs:
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run all tests with coverage report
|
||||
run: uv run poe test -A -C --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
|
||||
- name: Check coverage threshold
|
||||
run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
|
||||
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: |
|
||||
python/python-coverage.xml
|
||||
|
||||
@@ -34,13 +34,12 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
# Unit tests
|
||||
- name: Run all tests
|
||||
run: uv run poe test -A --junitxml=pytest.xml
|
||||
run: uv run poe all-tests
|
||||
working-directory: ./python
|
||||
|
||||
# Surface failing tests
|
||||
@@ -48,7 +47,7 @@ jobs:
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
path: ./python/**.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Stale issue and PR ping
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Midnight UTC daily
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_threshold:
|
||||
description: 'Days of silence before pinging the author'
|
||||
required: false
|
||||
default: '4'
|
||||
dry_run:
|
||||
description: 'Log what would be pinged without taking action'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- 'false'
|
||||
- 'true'
|
||||
|
||||
concurrency:
|
||||
group: stale-issue-pr-ping
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ping_stale:
|
||||
name: "Ping stale issues and PRs"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install PyGithub==2.6.0
|
||||
|
||||
- name: Run stale issue/PR ping
|
||||
run: python .github/scripts/stale_issue_pr_ping.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
TEAM_SLUG: ${{ secrets.DEVELOPER_TEAM }}
|
||||
DAYS_THRESHOLD: ${{ github.event.inputs.days_threshold || '4' }}
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||
+7
-21
@@ -47,8 +47,6 @@ htmlcov/
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
pytest.xml
|
||||
python-coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
@@ -136,10 +134,6 @@ celerybeat.pid
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
|
||||
# Foundry agent CLI (contains secrets, auto-generated)
|
||||
.foundry-agent.json
|
||||
.foundry-agent-build.log
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
@@ -205,27 +199,23 @@ temp*/
|
||||
.tmp/
|
||||
.temp/
|
||||
|
||||
agents.md
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
.omc/
|
||||
.omx/
|
||||
WARP.md
|
||||
**/memory-bank/
|
||||
**/projectBrief.md
|
||||
**/tmpclaude*
|
||||
# Dependency-bound validation reports
|
||||
python/scripts/dependency-*-results.json
|
||||
python/scripts/dependencies/dependency-*-results.json
|
||||
|
||||
# Azurite storage emulator files
|
||||
*/__azurite_db_blob__.json*
|
||||
*/__azurite_db_blob_extent__.json*
|
||||
*/__azurite_db_queue__.json*
|
||||
*/__azurite_db_queue_extent__.json*
|
||||
*/__azurite_db_table__.json*
|
||||
*/__azurite_db_blob__.json
|
||||
*/__azurite_db_blob_extent__.json
|
||||
*/__azurite_db_queue__.json
|
||||
*/__azurite_db_queue_extent__.json
|
||||
*/__azurite_db_table__.json
|
||||
*/__blobstorage__/
|
||||
*/__queuestorage__/
|
||||
*/AzuriteConfig
|
||||
|
||||
# Azure Functions local settings
|
||||
local.settings.json
|
||||
@@ -238,7 +228,3 @@ local.settings.json
|
||||
# Database files
|
||||
*.db
|
||||
python/dotnet-ref
|
||||
|
||||
# Generated filtered solution files (created by eng/scripts/New-FilteredSolution.ps1)
|
||||
dotnet/filtered-*.slnx
|
||||
**/*.lscache
|
||||
|
||||
+8
-47
@@ -74,37 +74,6 @@ Contributions must maintain API signature and behavioral compatibility. Contribu
|
||||
that include breaking changes will be rejected. Please file an issue to discuss
|
||||
your idea or change if you believe that a breaking change is warranted.
|
||||
|
||||
#### Automated API Compatibility Validation
|
||||
|
||||
The .NET projects use [Package Validation](https://learn.microsoft.com/dotnet/fundamentals/package-validation/overview)
|
||||
to automatically detect API breaking changes. This validation runs during `dotnet build`
|
||||
(Release configuration) and `dotnet pack`, comparing the current API surface against the
|
||||
latest published NuGet baseline version.
|
||||
|
||||
**What gets validated:** By default, packable RC packages (`IsReleaseCandidate=true`) and
|
||||
GA packages (`IsGenerallyAvailable=true`) that have a published NuGet baseline and do not
|
||||
override validation settings are automatically validated. The shared baseline version and
|
||||
default validation settings are defined in `dotnet/nuget/nuget-package.props`, but
|
||||
individual projects may opt out (for example by setting `EnablePackageValidation=false`).
|
||||
|
||||
**If the build fails with CP errors (e.g., CP0001, CP0002):**
|
||||
|
||||
1. **Unintentional breaking change** — Refactor your code to maintain backward compatibility.
|
||||
2. **Intentional breaking change** (approved by maintainers) — Generate a suppression file:
|
||||
```bash
|
||||
dotnet build <project>.csproj -c Release /p:ApiCompatGenerateSuppressionFile=true
|
||||
```
|
||||
This creates or updates a `CompatibilitySuppressions.xml` in the project directory.
|
||||
Include this file in your PR with justification for the breaking change.
|
||||
|
||||
**After each release:**
|
||||
|
||||
1. Delete all `CompatibilitySuppressions.xml` files from validated projects.
|
||||
2. Update `PackageValidationBaselineVersion` in `dotnet/nuget/nuget-package.props` to the
|
||||
newly published version.
|
||||
|
||||
For more details, see the [Package Validation diagnostic IDs](https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids).
|
||||
|
||||
### Suggested Workflow
|
||||
|
||||
We use and recommend the following workflow:
|
||||
@@ -123,30 +92,22 @@ We use and recommend the following workflow:
|
||||
"issue-123" or "githubhandle-issue".
|
||||
4. Make and commit your changes to your branch.
|
||||
5. Add new tests corresponding to your change, if applicable.
|
||||
6. Run the relevant scripts in [the section below](#development-setup) to ensure that your build is clean and all tests are passing.
|
||||
6. Run the relevant scripts in [the section below](#development-scripts) to ensure that your build is clean and all tests are passing.
|
||||
7. Create a PR against the repository's **main** branch.
|
||||
- State in the description what issue or improvement your change is addressing.
|
||||
- Verify that all the Continuous Integration checks are passing.
|
||||
8. Wait for feedback or approval of your changes from the code maintainers.
|
||||
9. When area owners have signed off, and all checks are green, your PR will be merged.
|
||||
|
||||
### Development Setup
|
||||
### Development scripts
|
||||
|
||||
Each language has its own dev setup guide, coding standards, and build scripts:
|
||||
The scripts below are used to build, test, and lint within the project.
|
||||
|
||||
- **Python**: [Dev Setup](./python/DEV_SETUP.md) · [Coding Standard](./python/CODING_STANDARD.md) · [README](./python/README.md)
|
||||
- From the `./python` directory:
|
||||
- Build: `uv run poe build`
|
||||
- Unit tests: `uv run poe test -A -m "not integration"`
|
||||
- Integration tests: `uv run poe test -A -m integration` (requires API keys/endpoints)
|
||||
- Format + lint: `uv run poe syntax`
|
||||
- All checks: `uv run poe check`
|
||||
- **.NET**: [README](./dotnet/README.md) · [Agent Instructions](./dotnet/AGENTS.md)
|
||||
- From the `./dotnet` directory:
|
||||
- Build: `dotnet build`
|
||||
- Unit tests: `dotnet test --filter-query "/*UnitTests*/*/*/*"`
|
||||
- Integration tests: `dotnet test --filter-query "/*IntegrationTests*/*/*/*"` (requires API keys/endpoints)
|
||||
- Linting (auto-fix): `dotnet format`
|
||||
- Python: see [python/DEV_SETUP.md](./python/DEV_SETUP.md).
|
||||
- .NET:
|
||||
- Build: `dotnet build`
|
||||
- Test: `dotnet test`
|
||||
- Linting (auto-fix): `dotnet format`
|
||||
|
||||
### PR - CI Process
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Welcome to Microsoft Agent Framework!
|
||||
|
||||
[](https://discord.gg/b5zjErwbQM)
|
||||
[](https://discord.gg/b5zjErwbQM)
|
||||
[](https://learn.microsoft.com/en-us/agent-framework/)
|
||||
[](https://pypi.org/project/agent-framework/)
|
||||
[](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
|
||||
@@ -28,7 +28,7 @@ Welcome to Microsoft's comprehensive multi-language framework for building, orch
|
||||
Python
|
||||
|
||||
```bash
|
||||
pip install agent-framework
|
||||
pip install agent-framework --pre
|
||||
# This will install all sub-packages, see `python/packages` for individual packages.
|
||||
# It may take a minute on first install on Windows.
|
||||
```
|
||||
@@ -53,7 +53,7 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
### ✨ **Highlights**
|
||||
|
||||
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, human-in-the-loop, and time-travel capabilities
|
||||
- [Python workflows](./python/samples/03-workflows/) | [.NET workflows](./dotnet/samples/03-workflows/)
|
||||
- [Python workflows](./python/samples/getting_started/workflows/) | [.NET workflows](./dotnet/samples/GettingStarted/Workflows/)
|
||||
- **AF Labs**: Experimental packages for cutting-edge features including benchmarking, reinforcement learning, and research initiatives
|
||||
- [Labs directory](./python/packages/lab/)
|
||||
- **DevUI**: Interactive developer UI for agent development, testing, and debugging workflows
|
||||
@@ -73,11 +73,11 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
|
||||
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
|
||||
- **Observability**: Built-in OpenTelemetry integration for distributed tracing, monitoring, and debugging
|
||||
- [Python observability](./python/samples/02-agents/observability/) | [.NET telemetry](./dotnet/samples/02-agents/AgentOpenTelemetry/)
|
||||
- [Python observability](./python/samples/getting_started/observability/) | [.NET telemetry](./dotnet/samples/GettingStarted/AgentOpenTelemetry/)
|
||||
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
|
||||
- [Python examples](./python/samples/02-agents/providers/) | [.NET examples](./dotnet/samples/02-agents/AgentProviders/)
|
||||
- [Python examples](./python/samples/getting_started/agents/) | [.NET examples](./dotnet/samples/GettingStarted/AgentProviders/)
|
||||
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
|
||||
- [Python middleware](./python/samples/02-agents/middleware/) | [.NET middleware](./dotnet/samples/02-agents/Agents/Agent_Step11_Middleware/)
|
||||
- [Python middleware](./python/samples/getting_started/middleware/) | [.NET middleware](./dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/)
|
||||
|
||||
### 💬 **We want your feedback!**
|
||||
|
||||
@@ -90,27 +90,27 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
Create a simple Azure Responses Agent that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```python
|
||||
# pip install agent-framework
|
||||
# pip install agent-framework --pre
|
||||
# Use `az login` to authenticate with Azure CLI
|
||||
import os
|
||||
import asyncio
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.azure import AzureOpenAIResponsesClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize a chat agent with Microsoft Foundry
|
||||
# Initialize a chat agent with Azure OpenAI Responses
|
||||
# the endpoint, deployment name, and api version can be set via environment variables
|
||||
# or they can be passed in directly to the FoundryChatClient constructor
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
credential=AzureCliCredential(),
|
||||
# project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
# model=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"],
|
||||
),
|
||||
name="HaikuBot",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
# or they can be passed in directly to the AzureOpenAIResponsesClient constructor
|
||||
agent = AzureOpenAIResponsesClient(
|
||||
# endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
||||
# deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
|
||||
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
||||
# api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential
|
||||
credential=AzureCliCredential(), # Optional, if using api_key
|
||||
).create_agent(
|
||||
name="HaikuBot",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
)
|
||||
|
||||
print(await agent.run("Write a haiku about Microsoft Agent Framework."))
|
||||
@@ -120,38 +120,37 @@ if __name__ == "__main__":
|
||||
```
|
||||
|
||||
### Basic Agent - .NET
|
||||
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.Foundry
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using System;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
using System;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(model: "gpt-5.4-mini", name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
// dotnet add package Azure.Identity
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using System;
|
||||
using OpenAI;
|
||||
|
||||
// Replace <resource> and gpt-4o-mini with your Azure OpenAI resource name and deployment name.
|
||||
var agent = new OpenAIClient(
|
||||
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
|
||||
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
@@ -160,43 +159,15 @@ 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 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, Azure Functions, Durable Task hosting
|
||||
- [End-to-End](./python/samples/05-end-to-end): full applications, evaluation, and demos
|
||||
- [Getting Started with Agents](./python/samples/getting_started/agents): basic agent creation and tool usage
|
||||
- [Chat Client Examples](./python/samples/getting_started/chat_client): direct chat client usage patterns
|
||||
- [Getting Started with Workflows](./python/samples/getting_started/workflows): basic workflow creation and integration with agents
|
||||
|
||||
### .NET
|
||||
|
||||
- [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, Durable Agents, Durable Workflows
|
||||
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Authentication errors when using Azure credentials | Not signed in to Azure CLI | Run `az login` before starting your app |
|
||||
| API key errors | Wrong or missing API key | Verify the key and ensure it's for the correct resource/provider |
|
||||
|
||||
> **Tip:** `DefaultAzureCredential` is convenient for development but in production, consider using a specific credential (e.g., `ManagedIdentityCredential`) to avoid latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The samples typically read configuration from environment variables. Common required variables:
|
||||
|
||||
| Variable | Used by | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI samples | Your Azure OpenAI resource URL |
|
||||
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Azure OpenAI samples | Model deployment name (e.g. `gpt-4o-mini`) |
|
||||
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry samples | Your Microsoft Foundry project endpoint |
|
||||
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Microsoft Foundry samples | Model deployment name |
|
||||
| `OPENAI_API_KEY` | OpenAI (non-Azure) samples | Your OpenAI platform API key |
|
||||
- [Getting Started with Agents](./dotnet/samples/GettingStarted/Agents): basic agent creation and tool usage
|
||||
- [Agent Provider Samples](./dotnet/samples/GettingStarted/AgentProviders): samples showing different agent providers
|
||||
- [Workflow Samples](./dotnet/samples/GettingStarted/Workflows): advanced multi-agent patterns and workflow orchestration
|
||||
|
||||
## Contributor Resources
|
||||
|
||||
@@ -207,9 +178,4 @@ The samples typically read configuration from environment variables. Common requ
|
||||
|
||||
## Important Notes
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you use Microsoft Agent Framework to build applications that operate with any third-party servers, agents, code, or non-Azure Direct models (“Third-Party Systems”), you do so at your own risk. Third-Party Systems are Non-Microsoft Products under the Microsoft Product Terms and are governed by their own third-party license terms. You are responsible for any usage and associated costs.
|
||||
>
|
||||
>We recommend reviewing all data being shared with and received from Third-Party Systems and being cognizant of third-party practices for handling, sharing, retention and location of data. It is your responsibility to manage whether your data will flow outside of your organization’s Azure compliance and geographic boundaries and any related implications, and that appropriate permissions, boundaries and approvals are provisioned.
|
||||
>
|
||||
>You are responsible for carefully reviewing and testing applications you build using Microsoft Agent Framework in the context of your specific use cases, and making all appropriate decisions and customizations. This includes implementing your own responsible AI mitigations such as metaprompt, content filters, or other safety systems, and ensuring your applications meet appropriate quality, reliability, security, and trustworthiness standards. See also: [Transparency FAQ](./TRANSPARENCY_FAQ.md)
|
||||
If you use the Microsoft Agent Framework to build applications that operate with third-party servers or agents, you do so at your own risk. We recommend reviewing all data being shared with third-party servers or agents and being cognizant of third-party practices for retention and location of data. It is your responsibility to manage whether your data will flow outside of your organization's Azure compliance and geographic boundaries and any related implications.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Declarative Agents
|
||||
|
||||
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
|
||||
+2
-2
@@ -3,13 +3,13 @@ name: MicrosoftLearnAgent
|
||||
description: Microsoft Learn Agent
|
||||
instructions: You answer questions by searching the Microsoft Learn content only.
|
||||
model:
|
||||
id: =Env.FOUNDRY_MODEL
|
||||
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: remote
|
||||
endpoint: =Env.FOUNDRY_PROJECT_ENDPOINT
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
tools:
|
||||
- kind: mcp
|
||||
name: microsoft_learn
|
||||
+1
-1
@@ -11,7 +11,7 @@ model:
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: key
|
||||
apiKey: =Env.OPENAI_API_KEY
|
||||
apiKey: =Env.OPENAI_APIKEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
@@ -1,3 +0,0 @@
|
||||
# Declarative Agents
|
||||
|
||||
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../../python/samples/02-agents/declarative/).
|
||||
@@ -1,17 +0,0 @@
|
||||
# Declarative Workflows
|
||||
|
||||
A _Declarative Workflow_ is defined as a single YAML file and
|
||||
may be executed locally no different from any regular `Workflow` that is defined by code.
|
||||
|
||||
The difference is that the workflow definition is loaded from a YAML file instead of being defined in code:
|
||||
|
||||
```c#
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build("Marketing.yaml", options);
|
||||
```
|
||||
|
||||
These example workflows may be executed by the workflow
|
||||
[Samples](../../dotnet/samples/03-workflows/Declarative)
|
||||
that are present in this repository.
|
||||
|
||||
> See the [README.md](../../dotnet/samples/03-workflows/Declarative/README.md)
|
||||
associated with the samples for configuration details.
|
||||
@@ -4,8 +4,8 @@ status: accepted
|
||||
contact: westey-m
|
||||
date: 2025-07-10 {YYYY-MM-DD when the decision was last updated}
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
|
||||
consulted:
|
||||
informed:
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# Agent Run Responses Design
|
||||
@@ -64,7 +64,7 @@ Approaches observed from the compared SDKs:
|
||||
| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
|
||||
| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
|
||||
| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
|
||||
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/docs/api/python/strands.agent.agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/docs/api/python/strands.agent.agent/) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
|
||||
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
|
||||
| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
|
||||
| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
|
||||
| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
|
||||
@@ -496,9 +496,9 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
|-|-|
|
||||
| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
|
||||
| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
|
||||
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/docs/api/python/strands.agent.agent/) |
|
||||
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
|
||||
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
|
||||
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/input-output/structured-output/agent) at agent construction time |
|
||||
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
|
||||
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
|
||||
| Protocol Activity | Supports returning [Complex types](https://github.com/microsoft/Agents/blob/main/specs/activity/protocol-activity.md#complex-types) but no support for requesting a type |
|
||||
|
||||
@@ -508,7 +508,7 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
|-|-|
|
||||
| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
|
||||
| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
|
||||
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/docs/api/python/strands.types.event_loop/) property on the [AgentResult](https://strandsagents.com/docs/api/python/strands.agent.agent_result/) class with options that are tied closely to LLM operations. |
|
||||
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
|
||||
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
|
||||
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
|
||||
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: proposed
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-01-08
|
||||
deciders: eavanvalkenburg, markwallace-microsoft, sphenry, alliscode, johanst, brettcannon
|
||||
consulted: taochenosu, moonbox3, dmytrostruk, giles17
|
||||
---
|
||||
|
||||
# Leveraging TypedDict and Generic Options in Python Chat Clients
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The Agent Framework Python SDK provides multiple chat client implementations for different providers (OpenAI, Anthropic, Azure AI, Bedrock, Ollama, etc.). Each provider has unique configuration options beyond the common parameters defined in `ChatOptions`. Currently, developers using these clients lack type safety and IDE autocompletion for provider-specific options, leading to runtime errors and a poor developer experience.
|
||||
|
||||
How can we provide type-safe, discoverable options for each chat client while maintaining a consistent API across all implementations?
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Type Safety**: Developers should get compile-time/static analysis errors when using invalid options
|
||||
- **IDE Support**: Full autocompletion and inline documentation for all available options
|
||||
- **Extensibility**: Users should be able to define custom options that extend provider-specific options
|
||||
- **Consistency**: All chat clients should follow the same pattern for options handling
|
||||
- **Provider Flexibility**: Each provider can expose its unique options without affecting the common interface
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Option 1: Status Quo - Class `ChatOptions` with `**kwargs`**
|
||||
- **Option 2: TypedDict with Generic Type Parameters**
|
||||
|
||||
### Option 1: Status Quo - Class `ChatOptions` with `**kwargs`
|
||||
|
||||
The current approach uses a base `ChatOptions` Class with common parameters, and provider-specific options are passed via `**kwargs` or loosely typed dictionaries.
|
||||
|
||||
```python
|
||||
# Current usage - no type safety for provider-specific options
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
top_k=40,
|
||||
random=42, # No validation
|
||||
)
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Simple implementation
|
||||
- Maximum flexibility
|
||||
|
||||
**Cons:**
|
||||
- No type checking for provider-specific options
|
||||
- No IDE autocompletion for available options
|
||||
- Runtime errors for typos or invalid options
|
||||
- Documentation must be consulted for each provider
|
||||
|
||||
### Option 2: TypedDict with Generic Type Parameters (Chosen)
|
||||
|
||||
Each chat client is parameterized with a TypeVar bound to a provider-specific `TypedDict` that extends `ChatOptions`. This enables full type safety and IDE support.
|
||||
|
||||
```python
|
||||
# Provider-specific TypedDict
|
||||
class AnthropicChatOptions(ChatOptions, total=False):
|
||||
"""Anthropic-specific chat options."""
|
||||
top_k: int
|
||||
thinking: ThinkingConfig
|
||||
# ... other Anthropic-specific options
|
||||
|
||||
# Generic chat client
|
||||
class AnthropicChatClient(ChatClientBase[TAnthropicChatOptions]):
|
||||
...
|
||||
|
||||
client = AnthropicChatClient(...)
|
||||
|
||||
# Usage with full type safety
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"random": 42, # fails type checking and IDE would flag this
|
||||
}
|
||||
)
|
||||
|
||||
# Users can extend for custom options
|
||||
class MyAnthropicOptions(AnthropicChatOptions, total=False):
|
||||
custom_field: str
|
||||
|
||||
|
||||
client = AnthropicChatClient[MyAnthropicOptions](...)
|
||||
|
||||
# Usage of custom options with full type safety
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"custom_field": "value",
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Full type safety with static analysis
|
||||
- IDE autocompletion for all options
|
||||
- Compile-time error detection
|
||||
- Self-documenting through type hints
|
||||
- Users can extend options for their specific needs or advances in models
|
||||
|
||||
**Cons:**
|
||||
- More complex implementation
|
||||
- Some type: ignore comments needed for TypedDict field overrides
|
||||
- Minor: Requires TypeVar with default (Python 3.13+ or typing_extensions)
|
||||
|
||||
> [NOTE!]
|
||||
> In .NET this is already achieved through overloads on the `GetResponseAsync` method for each provider-specific options class, e.g., `AnthropicChatOptions`, `OpenAIChatOptions`, etc. So this does not apply to .NET.
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **Base Protocol**: `ChatClientProtocol[TOptions]` is generic over options type, with default set to `ChatOptions` (the new TypedDict)
|
||||
2. **Provider TypedDicts**: Each provider defines its options extending `ChatOptions`
|
||||
They can even override fields with type=None to indicate they are not supported.
|
||||
3. **TypeVar Pattern**: `TProviderOptions = TypeVar("TProviderOptions", bound=TypedDict, default=ProviderChatOptions, contravariant=True)`
|
||||
4. **Option Translation**: Common options are kept in place,and explicitly documented in the Options class how they are used. (e.g., `user` → `metadata.user_id`) in `_prepare_options` (for Anthropic) to preserve easy use of common options.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.
|
||||
|
||||
See [typed_options.py](../../python/samples/getting_started/chat_client/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: proposed
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-01-08
|
||||
deciders: eavanvalkenburg, markwallace-microsoft, sphenry, alliscode, johanst, brettcannon
|
||||
consulted: taochenosu, moonbox3, dmytrostruk, giles17
|
||||
---
|
||||
|
||||
# Leveraging TypedDict and Generic Options in Python Chat Clients
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The Agent Framework Python SDK provides multiple chat client implementations for different providers (OpenAI, Anthropic, Azure AI, Bedrock, Ollama, etc.). Each provider has unique configuration options beyond the common parameters defined in `ChatOptions`. Currently, developers using these clients lack type safety and IDE autocompletion for provider-specific options, leading to runtime errors and a poor developer experience.
|
||||
|
||||
How can we provide type-safe, discoverable options for each chat client while maintaining a consistent API across all implementations?
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Type Safety**: Developers should get compile-time/static analysis errors when using invalid options
|
||||
- **IDE Support**: Full autocompletion and inline documentation for all available options
|
||||
- **Extensibility**: Users should be able to define custom options that extend provider-specific options
|
||||
- **Consistency**: All chat clients should follow the same pattern for options handling
|
||||
- **Provider Flexibility**: Each provider can expose its unique options without affecting the common interface
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Option 1: Status Quo - Class `ChatOptions` with `**kwargs`**
|
||||
- **Option 2: TypedDict with Generic Type Parameters**
|
||||
|
||||
### Option 1: Status Quo - Class `ChatOptions` with `**kwargs`
|
||||
|
||||
The current approach uses a base `ChatOptions` Class with common parameters, and provider-specific options are passed via `**kwargs` or loosely typed dictionaries.
|
||||
|
||||
```python
|
||||
# Current usage - no type safety for provider-specific options
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
top_k=40,
|
||||
random=42, # No validation
|
||||
)
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Simple implementation
|
||||
- Maximum flexibility
|
||||
|
||||
**Cons:**
|
||||
- No type checking for provider-specific options
|
||||
- No IDE autocompletion for available options
|
||||
- Runtime errors for typos or invalid options
|
||||
- Documentation must be consulted for each provider
|
||||
|
||||
### Option 2: TypedDict with Generic Type Parameters (Chosen)
|
||||
|
||||
Each chat client is parameterized with a TypeVar bound to a provider-specific `TypedDict` that extends `ChatOptions`. This enables full type safety and IDE support.
|
||||
|
||||
```python
|
||||
# Provider-specific TypedDict
|
||||
class AnthropicChatOptions(ChatOptions, total=False):
|
||||
"""Anthropic-specific chat options."""
|
||||
top_k: int
|
||||
thinking: ThinkingConfig
|
||||
# ... other Anthropic-specific options
|
||||
|
||||
# Generic chat client
|
||||
class AnthropicChatClient(ChatClientBase[TAnthropicChatOptions]):
|
||||
...
|
||||
|
||||
client = AnthropicChatClient(...)
|
||||
|
||||
# Usage with full type safety
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"random": 42, # fails type checking and IDE would flag this
|
||||
}
|
||||
)
|
||||
|
||||
# Users can extend for custom options
|
||||
class MyAnthropicOptions(AnthropicChatOptions, total=False):
|
||||
custom_field: str
|
||||
|
||||
|
||||
client = AnthropicChatClient[MyAnthropicOptions](...)
|
||||
|
||||
# Usage of custom options with full type safety
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"temperature": 0.7,
|
||||
"top_k": 40,
|
||||
"custom_field": "value",
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Full type safety with static analysis
|
||||
- IDE autocompletion for all options
|
||||
- Compile-time error detection
|
||||
- Self-documenting through type hints
|
||||
- Users can extend options for their specific needs or advances in models
|
||||
|
||||
**Cons:**
|
||||
- More complex implementation
|
||||
- Some type: ignore comments needed for TypedDict field overrides
|
||||
- Minor: Requires TypeVar with default (Python 3.13+ or typing_extensions)
|
||||
|
||||
> [NOTE!]
|
||||
> In .NET this is already achieved through overloads on the `GetResponseAsync` method for each provider-specific options class, e.g., `AnthropicChatOptions`, `OpenAIChatOptions`, etc. So this does not apply to .NET.
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **Base Protocol**: `ChatClientProtocol[TOptions]` is generic over options type, with default set to `ChatOptions` (the new TypedDict)
|
||||
2. **Provider TypedDicts**: Each provider defines its options extending `ChatOptions`
|
||||
They can even override fields with type=None to indicate they are not supported.
|
||||
3. **TypeVar Pattern**: `TProviderOptions = TypeVar("TProviderOptions", bound=TypedDict, default=ProviderChatOptions, contravariant=True)`
|
||||
4. **Option Translation**: Common options are kept in place,and explicitly documented in the Options class how they are used. (e.g., `user` → `metadata.user_id`) in `_prepare_options` (for Anthropic) to preserve easy use of common options.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.
|
||||
|
||||
See [typed_options.py](../../python/samples/02-agents/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
|
||||
@@ -1,423 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2025-01-21
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, westey-m, stephentoub
|
||||
consulted: reubenbond
|
||||
informed:
|
||||
---
|
||||
|
||||
# Feature Collections
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When using agents, we often have cases where we want to pass some arbitrary services or data to an agent or some component in the agent execution stack.
|
||||
These services or data are not necessarily known at compile time and can vary by the agent stack that the user has built.
|
||||
E.g., there may be an agent decorator or chat client decorator that was added to the stack by the user, and an arbitrary payload needs to be passed to that decorator.
|
||||
|
||||
Since these payloads are related to components that are not integral parts of the agent framework, they cannot be added as strongly typed settings to the agent run options.
|
||||
However, the payloads could be added to the agent run options as loosely typed 'features', that can be retrieved as needed.
|
||||
|
||||
In some cases certain classes of agents may support the same capability, but not all agents do.
|
||||
Having the configuration for such a capability on the main abstraction would advertise the functionality to all users, even if their chosen agent does not support it.
|
||||
The user may type test for certain agent types, and call overloads on the appropriate agent types, with the strongly typed configuration.
|
||||
Having a feature collection though, would be an alternative way of passing such configuration, without needing to type check the agent type.
|
||||
All agents that support the functionality would be able to check for the configuration and use it, simplifying the user code.
|
||||
If the agent does not support the capability, that configuration would be ignored.
|
||||
|
||||
### Sample Scenario 1 - Per Run ChatMessageStore Override for hosting Libraries
|
||||
|
||||
We are building an agent hosting library, that can host any agent built using the agent framework.
|
||||
Where an agent is not built on a service that uses in-service chat history storage, the hosting library wants to force the agent to use
|
||||
the hosting library's chat history storage implementation.
|
||||
This chat history storage implementation may be specifically tailored to the type of protocol that the hosting library uses, e.g. conversation id based storage or response id based storage.
|
||||
The hosting library does not know what type of agent it is hosting, so it cannot provide a strongly typed parameter on the agent.
|
||||
Instead, it adds the chat history storage implementation to a feature collection, and if the agent supports custom chat history storage, it retrieves the implementation from the feature collection and uses it.
|
||||
|
||||
```csharp
|
||||
// Pseudo-code for an agent hosting library that supports conversation id based hosting.
|
||||
public async Task<string> HandleConversationsBasedRequestAsync(AIAgent agent, string conversationId, string userInput)
|
||||
{
|
||||
var thread = await this._threadStore.GetOrCreateThread(conversationId);
|
||||
|
||||
// The hosting library can set a per-run chat message store via Features that only applies for that run.
|
||||
// This message store will load and save messages under the conversation id provided.
|
||||
ConversationsChatMessageStore messageStore = new(this._dbClient, conversationId);
|
||||
var response = await agent.RunAsync(
|
||||
userInput,
|
||||
thread,
|
||||
options: new AgentRunOptions()
|
||||
{
|
||||
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
|
||||
});
|
||||
|
||||
await this._threadStore.SaveThreadAsync(conversationId, thread);
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
// Pseudo-code for an agent hosting library that supports response id based hosting.
|
||||
public async Task<(string responseMessage, string responseId)> HandleResponseIdBasedRequestAsync(AIAgent agent, string previousResponseId, string userInput)
|
||||
{
|
||||
var thread = await this._threadStore.GetOrCreateThreadAsync(previousResponseId);
|
||||
|
||||
// The hosting library can set a per-run chat message store via Features that only applies for that run.
|
||||
// This message store will buffer newly added messages until explicitly saved after the run.
|
||||
ResponsesChatMessageStore messageStore = new(this._dbClient, previousResponseId);
|
||||
|
||||
var response = await agent.RunAsync(
|
||||
userInput,
|
||||
thread,
|
||||
options: new AgentRunOptions()
|
||||
{
|
||||
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
|
||||
});
|
||||
|
||||
// Since the message store may not actually have been used at all (if the agent's underlying chat client requires service-based chat history storage),
|
||||
// we may not have anything to save back to the database.
|
||||
// We still want to generate a new response id though, so that we can save the updated thread state under that id.
|
||||
// We should also use the same id to save any buffered messages in the message store if there are any.
|
||||
var newResponseId = this.GenerateResponseId();
|
||||
if (messageStore.HasBufferedMessages)
|
||||
{
|
||||
await messageStore.SaveBufferedMessagesAsync(newResponseId);
|
||||
}
|
||||
|
||||
// Save the updated thread state under the new response id that was generated by the store.
|
||||
await this._threadStore.SaveThreadAsync(newResponseId, thread);
|
||||
return (response.Text, newResponseId);
|
||||
}
|
||||
```
|
||||
|
||||
### Sample Scenario 2 - Structured output
|
||||
|
||||
Currently our base abstraction does not support structured output, since the capability is not supported by all agents.
|
||||
For those agents that don't support structured output, we could add an agent decorator that takes the response from the underlying agent, and applies structured output parsing on top of it via an additional LLM call.
|
||||
|
||||
If we add structured output configuration as a feature, then any agent that supports structured output could retrieve the configuration from the feature collection and apply it, and where it is not supported, the configuration would simply be ignored.
|
||||
|
||||
We could add a simple StructuredOutputAgentFeature that can be added to the list of features and also be used to return the generated structured output.
|
||||
|
||||
```csharp
|
||||
internal class StructuredOutputAgentFeature
|
||||
{
|
||||
public Type? OutputType { get; set; }
|
||||
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
|
||||
public bool? UseJsonSchemaResponseFormat { get; set; }
|
||||
|
||||
// Contains the result of the structured output parsing request.
|
||||
public ChatResponse? ChatResponse { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
We can add a simple decorator class that does the chat client invocation.
|
||||
|
||||
```csharp
|
||||
public class StructuredOutputAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._chatClient = Throw.IfNull(chatClient);
|
||||
}
|
||||
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Run the inner agent first, to get back the text response we want to convert.
|
||||
var response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (options?.Features?.TryGet<StructuredOutputAgentFeature>(out var responseFormatFeature) is true
|
||||
&& responseFormatFeature.OutputType is not null)
|
||||
{
|
||||
// Create the chat options to request structured output.
|
||||
ChatOptions chatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema(responseFormatFeature.OutputType, responseFormatFeature.SerializerOptions)
|
||||
};
|
||||
|
||||
// Invoke the chat client to transform the text output into structured data.
|
||||
// The feature is updated with the result.
|
||||
// The code can be simplified by adding a non-generic structured output GetResponseAsync
|
||||
// overload that takes Type as input.
|
||||
responseFormatFeature.ChatResponse = await this._chatClient.GetResponseAsync(
|
||||
messages: new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.System, "You are a json expert and when provided with any text, will convert it to the requested json format."),
|
||||
new ChatMessage(ChatRole.User, response.Text)
|
||||
},
|
||||
options: chatOptions,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we can add an extension method on `AIAgent` that can add the feature to the run options and check the feature for the structured output result and add the deserialized result to the response.
|
||||
|
||||
```csharp
|
||||
public static async Task<AgentRunResponse<T>> RunAsync<T>(
|
||||
this AIAgent agent,
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
bool? useJsonSchemaResponseFormat = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create the structured output feature.
|
||||
var structuredOutputFeature = new StructuredOutputAgentFeature();
|
||||
structuredOutputFeature.OutputType = typeof(T);
|
||||
structuredOutputFeature.UseJsonSchemaResponseFormat = useJsonSchemaResponseFormat;
|
||||
|
||||
// Run the agent.
|
||||
options ??= new AgentRunOptions();
|
||||
options.Features ??= new AgentFeatureCollection();
|
||||
options.Features.Set(structuredOutputFeature);
|
||||
|
||||
var response = await agent.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Deserialize the JSON output.
|
||||
if (structuredOutputFeature.ChatResponse is not null)
|
||||
{
|
||||
var typed = new ChatResponse<T>(structuredOutputFeature.ChatResponse, serializerOptions ?? AgentJsonUtilities.DefaultOptions);
|
||||
return new AgentRunResponse<T>(response, typed.Result);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No structured output response was generated by the agent.");
|
||||
}
|
||||
```
|
||||
|
||||
We can then use the extension method with any agent that supports structured output or that has
|
||||
been decorated with the `StructuredOutputAgent` decorator.
|
||||
|
||||
```csharp
|
||||
agent = new StructuredOutputAgent(agent, chatClient);
|
||||
|
||||
AgentRunResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>([new ChatMessage(
|
||||
ChatRole.User,
|
||||
"Please provide information about John Smith, who is a 35-year-old software engineer.")]);
|
||||
```
|
||||
|
||||
## Implementation Options
|
||||
|
||||
Three options were considered for implementing feature collections:
|
||||
|
||||
- **Option 1**: FeatureCollections similar to ASP.NET Core
|
||||
- **Option 2**: AdditionalProperties Dictionary
|
||||
- **Option 3**: IServiceProvider
|
||||
|
||||
Here are some comparisons about their suitability for our use case:
|
||||
|
||||
| Criteria | Feature Collection | Additional Properties | IServiceProvider |
|
||||
|------------------|--------------------|-----------------------|------------------|
|
||||
|Ease of use |✅ Good |❌ Bad |✅ Good |
|
||||
|User familiarity |❌ Bad |✅ Good |✅ Good |
|
||||
|Type safety |✅ Good |❌ Bad |✅ Good |
|
||||
|Ability to modify registered options when progressing down the stack|✅ Supported|✅ Supported|❌ Not-Supported (IServiceProvider is read-only)|
|
||||
|Already available in MEAI stack|❌ No|✅ Yes|❌ No|
|
||||
|Ambiguity with existing AdditionalProperties|❌ Yes|✅ No|❌ Yes|
|
||||
|
||||
## IServiceProvider
|
||||
|
||||
Service Collections and Service Providers provide a very popular way to register and retrieve services by type and could be used as a way to pass features to agents and chat clients.
|
||||
|
||||
However, since IServiceProvider is read-only, it is not possible to modify the registered services when progressing down the execution stack.
|
||||
E.g. an agent decorator cannot add additional services to the IServiceProvider passed to it when calling into the inner agent.
|
||||
|
||||
IServiceProvider also does not expose a way to list all services contained in it, making it difficult to copy services from one provider to another.
|
||||
|
||||
This lack of mutability makes IServiceProvider unsuitable for our use case, since we will not be able to use it to build sample scenario 2.
|
||||
|
||||
## AdditionalProperties dictionary
|
||||
|
||||
The AdditionalProperties dictionary is already available on various options classes in the agent framework as well as in the MEAI stack and
|
||||
allows storing arbitrary key/value pairs, where the key is a string and the value is an object.
|
||||
|
||||
While FeatureCollection uses Type as a key, AdditionalProperties uses string keys.
|
||||
This means that users need to agree on string keys to use for specific features, however it is also possible to use Type.FullName as a key by convention
|
||||
to avoid key collisions, which is an easy convention to follow.
|
||||
|
||||
Since the value of AdditionalProperties is of type object, users need to cast the value to the expected type when retrieving it, which is also
|
||||
a drawback, but when using the convention of using Type.FullName as a key, there is at least a clear expectation of what type to cast to.
|
||||
|
||||
```csharp
|
||||
// Setting a feature
|
||||
options.AdditionalProperties[typeof(MyFeature).FullName] = new MyFeature();
|
||||
|
||||
// Retrieving a feature
|
||||
if (options.AdditionalProperties.TryGetValue(typeof(MyFeature).FullName, out var featureObj)
|
||||
&& featureObj is MyFeature myFeature)
|
||||
{
|
||||
// Use myFeature
|
||||
}
|
||||
```
|
||||
|
||||
It would also be possible to add extension methods to simplify setting and getting features from AdditionalProperties.
|
||||
Having a base class for features should help make this more feature rich.
|
||||
|
||||
```csharp
|
||||
// Setting a feature, this can use Type.FullName as the key.
|
||||
options.AdditionalProperties
|
||||
.WithFeature(new MyFeature());
|
||||
|
||||
// Retrieving a feature, this can use Type.FullName as the key.
|
||||
if (options.AdditionalProperties.TryGetFeature<MyFeature>(out var myFeature))
|
||||
{
|
||||
// Use myFeature
|
||||
}
|
||||
```
|
||||
|
||||
It would also be possible to add extension methods for a feature to simplify setting and getting features from AdditionalProperties.
|
||||
|
||||
```csharp
|
||||
// Setting a feature
|
||||
options.AdditionalProperties
|
||||
.WithMyFeature(new MyFeature());
|
||||
// Retrieving a feature
|
||||
if (options.AdditionalProperties.TryGetMyFeature(out var myFeature))
|
||||
{
|
||||
// Use myFeature
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Collection
|
||||
|
||||
If we choose the feature collection option, we need to decide on the design of the feature collection itself.
|
||||
|
||||
### Feature Collections extension points
|
||||
|
||||
We need to decide the set of actions that feature collections would be supported for. Here is the suggested list of actions:
|
||||
|
||||
**MAAI.AIAgent:**
|
||||
|
||||
1. GetNewThread
|
||||
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
|
||||
1. DeserializeThread
|
||||
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
|
||||
1. Run / RunStreaming
|
||||
1. E.g. this would allow passing an override chat message store just for that run, or a desired schema for a structured output middleware component.
|
||||
|
||||
**MEAI.ChatClient:**
|
||||
|
||||
1. GetResponse / GetStreamingResponse
|
||||
|
||||
### Reconciling with existing AdditionalProperties
|
||||
|
||||
If we decide to add feature collections, separately from the existing AdditionalProperties dictionaries, we need to consider how to explain to users when to use each one.
|
||||
One possible approach though is to have the one use the other under the hood.
|
||||
AdditionalProperties could be stored as a feature in the feature collection.
|
||||
|
||||
Users would be able to retrieve additional properties from the feature collection, in addition to retrieving it via a dedicated AdditionalProperties property.
|
||||
E.g. `features.Get<AdditionalPropertiesDictionary>()`
|
||||
|
||||
One challenge with this approach is that when setting a value in the AdditionalProperties dictionary, the feature collection would need to be created first if it does not already exist.
|
||||
|
||||
```csharp
|
||||
public class AgentRunOptions
|
||||
{
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
public IAgentFeatureCollection? Features { get; set; }
|
||||
}
|
||||
|
||||
var options = new AgentRunOptions();
|
||||
// This would need to create the feature collection first, if it does not already exist.
|
||||
options.AdditionalProperties ??= new AdditionalPropertiesDictionary();
|
||||
```
|
||||
|
||||
Since IAgentFeatureCollection is an interface, AgentRunOptions would need to have a concrete implementation of the interface to create, meaning that the user cannot decide.
|
||||
It also means that if the user doesn't realise that AdditionalProperties is implemented using feature collections, they may set a value on AdditionalProperties, and then later overwrite the entire feature collection, losing the AdditionalProperties feature.
|
||||
|
||||
Options to avoid these issues:
|
||||
|
||||
1. Make `Features` readonly.
|
||||
1. This would prevent the user from overwriting the feature collection after setting AdditionalProperties.
|
||||
1. Since the user cannot set their own implementation of IAgentFeatureCollection, having an interface for it may not be necessary.
|
||||
|
||||
### Feature Collection Implementation
|
||||
|
||||
We have two options for implementing feature collections:
|
||||
|
||||
1. Create our own [IAgentFeatureCollection interface](https://github.com/microsoft/agent-framework/pull/2354/files#diff-9c42f3e60d70a791af9841d9214e038c6de3eebfc10e3997cb4cdffeb2f1246d) and [implementation](https://github.com/microsoft/agent-framework/pull/2354/files#diff-a435cc738baec500b8799f7f58c1538e3bb06c772a208afc2615ff90ada3f4ca).
|
||||
2. Reuse the asp.net [IFeatureCollection interface](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/IFeatureCollection.cs) and [implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs).
|
||||
|
||||
#### Roll our own
|
||||
|
||||
Advantages:
|
||||
|
||||
Creating our own IAgentFeatureCollection interface and implementation has the advantage of being more clearly associated with the agent framework and allows us to
|
||||
improve on some of the design decisions made in asp.net core's IFeatureCollection.
|
||||
|
||||
Drawbacks:
|
||||
|
||||
It would mean a different implementation to maintain and test.
|
||||
|
||||
#### Reuse asp.net IFeatureCollection
|
||||
|
||||
Advantages:
|
||||
|
||||
Reusing the asp.net IFeatureCollection has the advantage of being able to reuse the well-established and tested implementation from asp.net
|
||||
core. Users who are using agents in an asp.net core application may be able to pass feature collections from asp.net core to the agent framework directly.
|
||||
|
||||
Drawbacks:
|
||||
|
||||
While the package name is `Microsoft.Extensions.Features`, the namespaces of the types are `Microsoft.AspNetCore.Http.Features`, which may create confusion for users of agent framework who are not building web applications or services.
|
||||
Users may rightly ask: Why do I need to use a class from asp.net core when I'm not building a web application / service?
|
||||
|
||||
The current design has some design issues that would be good to avoid. E.g. it does not distinguish between a feature being "not set" and "null". Get returns both as null and there is no tryget method.
|
||||
Since the [default implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs) also supports value types, it throws for null values of value types.
|
||||
A TryGet method would be more appropriate.
|
||||
|
||||
## Feature Layering
|
||||
|
||||
One possible scenario when adding support for feature collections is to allow layering of features by scope.
|
||||
|
||||
The following levels of scope could be supported:
|
||||
|
||||
1. Application - Application wide features that apply to all agents / chat clients
|
||||
2. Artifact (Agent / ChatClient) - Features that apply to all runs of a specific agent or chat client instance
|
||||
3. Action (GetNewThread / Run / GetResponse) - Feature that apply to a single action only
|
||||
|
||||
When retrieving a feature from the collection, the search would start from the most specific scope (Action) and progress to the least specific scope (Application), returning the first matching feature found.
|
||||
|
||||
Introducing layering adds some challenges:
|
||||
|
||||
- There may be multiple feature collections at the same scope level, e.g. an Agent that uses a ChatClient where both have their own feature collections.
|
||||
- Do we layer the agent feature collection over the chat client feature collection (Application -> ChatClient -> Agent -> Run), or only use the agent feature collection in the agent (Application -> Agent -> Run), and the chat client feature collection in the chat client (Application -> ChatClient -> Run)?
|
||||
- The appropriate base feature collection may change when progressing down the stack, e.g. when an Agent calls a ChatClient, the action feature collection stays the same, but the artifact feature collection changes.
|
||||
- Who creates the feature collection hierarchy?
|
||||
- Since the hierarchy changes as it progresses down the execution stack, and the caller can only pass in the action level feature collection, the callee needs to combine it with its own artifact level feature collection and the application level feature collection. Each action will need to build the appropriate feature collection hierarchy, at the start of its execution.
|
||||
- For Artifact level features, it seems odd to pass them in as a bag of untyped features, when we are constructing a known artifact type and therefore can have typed settings.
|
||||
- E.g. today we have a strongly typed setting on ChatClientAgentOptions to configure a ChatMessageStore for the agent.
|
||||
- To avoid global statics for application level features, the user would need to pass in the application level feature collection to each artifact that they create.
|
||||
- This would be very odd if the user also already has to strongly typed settings for each feature that they want to set at the artifact level.
|
||||
|
||||
### Layering Options
|
||||
|
||||
1. No layering - only a single feature collection is supported per action (the caller can still create a layered collection if desired, but the callee does not do any layering automatically).
|
||||
1. Fallback is to any features configured on the artifact via strongly typed settings.
|
||||
1. Full layering - support layering at all levels (Application -> Artifact -> Action).
|
||||
1. Only apply applicable artifact level features when calling into that artifact.
|
||||
1. Apply upstream artifact features when calling into downstream artifacts, e.g. Feature hierarchy in ChatClientAgent would be `Application -> Agent -> Run` and in ChatClient would be `Application -> ChatClient -> Agent -> Run` or `Application -> Agent -> ChatClient -> Run`
|
||||
1. The user needs to provide the application level feature collection to each artifact that they create and artifact features are passed via strongly typed settings.
|
||||
|
||||
### Accessing application level features Options
|
||||
|
||||
We need to consider how application level features would be accessed if supported.
|
||||
|
||||
1. The user provides the application level feature collection to each artifact that the user constructs
|
||||
1. Passing the application level feature collection to each artifact is tedious for the user.
|
||||
1. There is a static application level feature collection that can be accessed globally.
|
||||
1. Statics create issues with testing and isolation.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Feature Collections Container: Use AdditionalProperties
|
||||
- Feature Layering: No layering - only a single collection/dictionary is supported per action. Application layers can be added later if needed.
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: westey-m
|
||||
date: 2026-01-27
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# AgentRunContext for Agent Run
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
During an agent run, various components involved in the execution (middleware, filters, tools, nested agents, etc.) may need access to contextual information about the current run, such as:
|
||||
|
||||
1. The agent that is executing the run
|
||||
2. The session associated with the run
|
||||
3. The request messages passed to the agent
|
||||
4. The run options controlling the agent's behavior
|
||||
|
||||
Additionally, some components may need to modify this context during execution, for example:
|
||||
|
||||
- Replacing the session with a different one
|
||||
- Modifying the request messages before they reach the agent core
|
||||
- Updating or replacing the run options entirely
|
||||
|
||||
Currently, there is no standardized way to access or modify this context from arbitrary code that executes during an agent run, especially from deeply nested call stacks where the context is not explicitly passed.
|
||||
|
||||
## Sample Scenario
|
||||
|
||||
When using an Agent as an AIFunction developers may want to pass context from the parent agent run to the child agent run. For example, the developer may want to copy chat history to the child agent, or share the same session across both agents.
|
||||
|
||||
To enable these scenarios, we need a way to access the parent agent run context, including e.g. the parent agent itself, the parent agent session, and the parent run options from function tool calls.
|
||||
|
||||
```csharp
|
||||
public static AIFunction AsAIFunctionWithSessionPropagation(this ChatClientAgent agent, AIFunctionFactoryOptions? options = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
|
||||
[Description("Invoke an agent to retrieve some information.")]
|
||||
async Task<string> InvokeAgentAsync(
|
||||
[Description("Input query to invoke the agent.")] string query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the session from the parent agent and pass it to the child agent.
|
||||
var session = AIAgent.CurrentRunContext?.Session;
|
||||
|
||||
// Alternatively, the developer may want to create a new session but copy over the chat history from the parent agent.
|
||||
// var parentChatHistory = AIAgent.CurrentRunContext?.Session?.GetService<IList<ChatMessage>>();
|
||||
// if (parentChatHistory != null)
|
||||
// {
|
||||
// var chp = new InMemoryChatHistoryProvider();
|
||||
// foreach (var message in parentChatHistory)
|
||||
// {
|
||||
// chp.Add(message);
|
||||
// }
|
||||
// session = agent.GetNewSession(chp);
|
||||
// }
|
||||
|
||||
var response = await agent.RunAsync(query, session: session, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
options ??= new();
|
||||
options.Name ??= SanitizeAgentName(agent.Name);
|
||||
options.Description ??= agent.Description;
|
||||
|
||||
return AIFunctionFactory.Create(InvokeAgentAsync, options);
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Components executing during an agent run need access to run context without explicit parameter passing through every layer
|
||||
- Context should flow naturally across async calls without manual propagation
|
||||
- The design should allow modification of context properties by agent decorators (e.g., replacing options or session)
|
||||
- Solution should be consistent with patterns used in similar frameworks (e.g., `FunctionInvokingChatClient.CurrentContext` `HttpContext.Current`, `Activity.Current`)
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Option 1**: Pass context explicitly through all method signatures
|
||||
- **Option 2**: Use `AsyncLocal<T>` to provide ambient context accessible anywhere during the run
|
||||
- **Option 3**: Use a combination of explicit parameters for `RunCoreAsync` and `AsyncLocal<T>` for ambient access
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **Option 3** - Combination of explicit parameters and AsyncLocal ambient access.
|
||||
|
||||
This approach provides the best of both worlds:
|
||||
|
||||
1. **Explicit parameters are passed to `RunCoreAsync`**: The core agent implementation receives the parameters explicitly, making it clear what data is available and enabling easy unit testing. Any modification of these in a decorator will require calling `RunAsync` on the inner agent with the updated parameters, which would result in the inner agent creating a new `AgentRunContext` instance.
|
||||
|
||||
```csharp
|
||||
public async Task<AgentResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
|
||||
return await this.RunCoreAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
```
|
||||
|
||||
2. **`AsyncLocal<AgentRunContext?>` for ambient access**: The context is stored in an `AsyncLocal<T>` field, making it accessible from any code executing during the agent run via a static property.
|
||||
|
||||
The main scenario for this is to allow deeply nested components (e.g., tools, chat client middleware) to access the context without needing to pass it through every method signature. These are external components that cannot easily be modified to accept additional parameters. For internal components, we prefer passing any parameters explicitly.
|
||||
|
||||
```csharp
|
||||
public static AgentRunContext? CurrentRunContext
|
||||
{
|
||||
get => s_currentContext.Value;
|
||||
protected set => s_currentContext.Value = value;
|
||||
}
|
||||
```
|
||||
|
||||
### AgentRunContext Design
|
||||
|
||||
The `AgentRunContext` class encapsulates all run-related state:
|
||||
|
||||
```csharp
|
||||
public class AgentRunContext
|
||||
{
|
||||
public AgentRunContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IReadOnlyCollection<ChatMessage> requestMessages,
|
||||
AgentRunOptions? agentRunOptions)
|
||||
|
||||
public AIAgent Agent { get; }
|
||||
public AgentSession? Session { get; }
|
||||
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
|
||||
public AgentRunOptions? RunOptions { get; }
|
||||
}
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
|
||||
- **All properties are read-only**: While some of the sub-properties on the provided properties (like `AgentRunOptions.AllowBackgroundResponses`) may be mutable, the `AgentRunContext` itself is immutable and we want to discourage anyone modifying the values in the context. Modifying the context is unlikely to result in the desired behavior, as the values will typically already have been used by the time any custom code accesses them.
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Ambient Access**: Any code executing during the run can access context via `AIAgent.CurrentRunContext` without needing explicit parameters
|
||||
2. **Async Flow**: `AsyncLocal<T>` automatically flows across async/await boundaries
|
||||
3. **Modifiability**: Components can modify or replace session, messages, or options as needed
|
||||
4. **Testability**: The explicit parameter to `RunCoreAsync` makes unit testing straightforward
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,658 +0,0 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: sergeymenshykh
|
||||
date: 2026-01-22
|
||||
deciders: rbarreto, westey-m, stephentoub
|
||||
informed: {}
|
||||
---
|
||||
|
||||
# Structured Output
|
||||
|
||||
Structured output is a valuable aspect of any agent system, since it forces an agent to produce output in a required format that may include required fields.
|
||||
This allows easily turning unstructured data into structured data using a general-purpose language model.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Structured output is currently supported only by `ChatClientAgent` and can be configured in two ways:
|
||||
|
||||
**Approach 1: ResponseFormat + Deserialize**
|
||||
|
||||
Specify the SO type schema via the `ChatClientAgent{Run}Options.ChatOptions.ResponseFormat` property at agent creation or invocation time, then use `JsonSerializer.Deserialize<T>` to extract the structured data from the response text.
|
||||
|
||||
```csharp
|
||||
// SO type can be provided at agent creation time
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "...",
|
||||
ChatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...");
|
||||
|
||||
PersonInfo personInfo = response.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
|
||||
// Alternatively, SO type can be provided at agent invocation time
|
||||
response = await agent.RunAsync("...", new ChatClientAgentRunOptions()
|
||||
{
|
||||
ChatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
|
||||
personInfo = response.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
```
|
||||
|
||||
**Approach 2: Generic RunAsync<T>**
|
||||
|
||||
Supply the SO type as a generic parameter to `RunAsync<T>` and access the parsed result directly via the `Result` property.
|
||||
|
||||
```csharp
|
||||
ChatClientAgent agent = ...;
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("...");
|
||||
|
||||
Console.WriteLine($"Name: {response.Result.Name}");
|
||||
Console.WriteLine($"Age: {response.Result.Age}");
|
||||
Console.WriteLine($"Occupation: {response.Result.Occupation}");
|
||||
```
|
||||
Note: `RunAsync<T>` is an instance method of `ChatClientAgent` and not part of the `AIAgent` base class since not all agents support structured output.
|
||||
|
||||
Approach 1 is perceived as cumbersome by the community, as it requires additional effort when using primitive or collection types - the SO schema may need to be wrapped in an artificial JSON object. Otherwise, the caller will encounter an error like _Invalid schema for response_format 'Movie': schema must be a JSON Schema of 'type: "object"', got 'type: "array"'_.
|
||||
This occurs because OpenAI and compatible APIs require a JSON object as the root schema.
|
||||
|
||||
Approach 1 is also necessary in scenarios where (a) agents can only be configured with SO at creation time (such as with `AIProjectClient`), (b) the SO type is not known at compile time, or (c) the JSON schema is represented as text (for declarative agents) or as a `JsonElement`.
|
||||
|
||||
Approach 2 is more convenient and works seamlessly with primitives and collections. However, it requires the SO type to be known at compile time, making it less flexible.
|
||||
|
||||
Additionally, since the `RunAsync<T>` methods are instance methods of `ChatClientAgent` and are not part of the `AIAgent` base class, applying decorators like `OpenTelemetryAgent` on top of `ChatClientAgent` prevents users from accessing `RunAsync<T>`, meaning structured output is not available with decorated agents.
|
||||
|
||||
Given the different scenarios above in which structured output can be used, there is no one-size-fits-all solution. Each approach has its own advantages and limitations,
|
||||
and the two can complement each other to provide a comprehensive structured output experience across various use cases.
|
||||
|
||||
## Approaches Overview
|
||||
|
||||
1. SO usage via `ResponseFormat` property
|
||||
2. SO usage via `RunAsync<T>` generic method
|
||||
|
||||
## 1. SO usage via `ResponseFormat` property
|
||||
|
||||
This approach should be used in the following scenarios:
|
||||
- 1.1 SO result as text is sufficient as is, and deserialization is not required
|
||||
- 1.2 SO for inter-agent collaboration
|
||||
- 1.3 SO can only be configured at agent creation time (such as with `AIProjectClient`)
|
||||
- 1.4 SO type is not known at compile time and represented by System.Type
|
||||
- 1.5 SO is represented by JSON schema and there's no corresponding .NET type either at compile time or at runtime
|
||||
- 1.6 SO in streaming scenarios, where the SO response is produced in parts
|
||||
|
||||
**Note: Primitives and arrays are not supported by this approach.**
|
||||
|
||||
When a caller provides a schema via `ResponseFormat`, they are explicitly telling the framework what schema to use. The framework passes that schema through as-is and
|
||||
is not responsible for transforming it. Because the framework does not own the schema, it cannot wrap primitives or arrays into a JSON object to satisfy API requirements,
|
||||
nor can it unwrap the response afterward - the caller controls the schema and is responsible for ensuring it is compatible with the underlying API.
|
||||
|
||||
This is in contrast to the `RunAsync<T>` approach (section 2), where the caller provides a type `T` and says "make it work." In that case, the caller does not
|
||||
dictate the schema - the framework infers the schema from `T`, owns the end-to-end pipeline (schema generation, API invocation, and deserialization), and can
|
||||
therefore wrap and unwrap primitives and arrays transparently.
|
||||
|
||||
Additionally, in streaming scenarios (1.6), the framework cannot reliably unwrap a response it did not wrap, since it has no way of knowing whether the caller wrapped the schema.Wrapping and unwrapping can only be done safely when the framework owns the entire lifecycle - from schema creation through deserialization — which is only the case with `RunAsync<T>`.
|
||||
|
||||
If a caller needs to work with primitives or arrays via the `ResponseFormat` approach, they can easily create a wrapper type around them:
|
||||
|
||||
```csharp
|
||||
public class MovieListWrapper
|
||||
{
|
||||
public List<string> Movies { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 1.1 SO result as text is sufficient as is, and deserialization is not required
|
||||
|
||||
In this scenario, the caller only needs the raw JSON text returned by the model and does not need to deserialize it into a .NET type.
|
||||
The SO schema is specified via `ResponseFormat` at agent creation or invocation time, and the response text is consumed directly from the `AgentResponse`.
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent();
|
||||
|
||||
AgentRunOptions runOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
|
||||
};
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...", options: runOptions);
|
||||
|
||||
Console.WriteLine(response.Text);
|
||||
```
|
||||
|
||||
### 1.2 SO for inter-agent collaboration
|
||||
|
||||
This scenario assumes a multi-agent setup where agents collaborate by passing messages to each other.
|
||||
One agent produces structured output as text that is then passed directly as input to the next agent, without intermediate deserialization.
|
||||
|
||||
```csharp
|
||||
// First agent extracts structured data from unstructured input
|
||||
AIAgent extractionAgent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "ExtractionAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "Extract person information from the provided text.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
|
||||
}
|
||||
});
|
||||
|
||||
AgentResponse extractionResponse = await extractionAgent.RunAsync("John Smith is a 35-year-old software engineer.");
|
||||
|
||||
// Pass the message with structured output text directly to the next agent
|
||||
ChatMessage soMessage = extractionResponse.Messages.Last();
|
||||
|
||||
AIAgent summaryAgent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "SummaryAgent",
|
||||
ChatOptions = new() { Instructions = "Given the following structured person data, write a short professional bio." }
|
||||
});
|
||||
|
||||
AgentResponse summaryResponse = await summaryAgent.RunAsync(soMessage);
|
||||
|
||||
Console.WriteLine(summaryResponse);
|
||||
```
|
||||
|
||||
### 1.3 SO configured at agent creation time
|
||||
|
||||
In this scenario, the SO schema can only be configured at agent creation time (such as with `AIProjectClient`) and cannot be changed on a per-run basis.
|
||||
The caller specifies the `ResponseFormat` when creating the agent, and all subsequent invocations use the same schema.
|
||||
|
||||
```csharp
|
||||
AIProjectClient client = ...;
|
||||
|
||||
AIAgent agent = await client.CreateAIAgentAsync(model: "<model>", new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "...",
|
||||
ChatOptions = new() { ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>() }
|
||||
});
|
||||
|
||||
AgentResponse response = await agent.RunAsync("Please provide information about John Smith.");
|
||||
|
||||
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>(response.Text, JsonSerializerOptions.Web)!;
|
||||
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
Console.WriteLine($"Age: {personInfo.Age}");
|
||||
Console.WriteLine($"Occupation: {personInfo.Occupation}");
|
||||
```
|
||||
|
||||
### 1.4 SO type not known at compile time and represented by System.Type
|
||||
|
||||
In this scenario, the SO type is not known at compile time and is provided as a `System.Type` at runtime. This is useful for dynamic scenarios where the schema is determined programmatically,
|
||||
such as when building tooling or frameworks that work with user-defined types.
|
||||
|
||||
```csharp
|
||||
Type soType = GetStructuredOutputTypeFromConfiguration(); // e.g., typeof(PersonInfo)
|
||||
|
||||
ChatResponseFormat responseFormat = ChatResponseFormat.ForJsonSchema(soType);
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...", new ChatClientAgentRunOptions()
|
||||
{
|
||||
ChatOptions = new() { ResponseFormat = responseFormat }
|
||||
});
|
||||
|
||||
PersonInfo personInfo = (PersonInfo)JsonSerializer.Deserialize(response.Text, soType, JsonSerializerOptions.Web)!;
|
||||
```
|
||||
|
||||
### 1.5 SO represented by JSON schema with no corresponding .NET type
|
||||
|
||||
In this scenario, the SO schema is represented as raw JSON schema text or a `JsonElement`, and there is no corresponding .NET type available at compile time or runtime.
|
||||
This is typical for declarative agents or scenarios where schemas are loaded from external configuration.
|
||||
|
||||
```csharp
|
||||
// JSON schema provided as a string, e.g., loaded from a configuration file
|
||||
string jsonSchema = """
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"age": { "type": "integer" },
|
||||
"occupation": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "age", "occupation"]
|
||||
}
|
||||
""";
|
||||
|
||||
ChatResponseFormat responseFormat = ChatResponseFormat.ForJsonSchema(
|
||||
jsonSchemaName: "PersonInfo",
|
||||
jsonSchema: BinaryData.FromString(jsonSchema));
|
||||
|
||||
AgentResponse response = await agent.RunAsync("...", new ChatClientAgentRunOptions()
|
||||
{
|
||||
ChatOptions = new() { ResponseFormat = responseFormat }
|
||||
});
|
||||
|
||||
// Consume the SO result as text since there's no .NET type to deserialize into
|
||||
Console.WriteLine(response.Text);
|
||||
```
|
||||
|
||||
### 1.6 SO in streaming scenarios
|
||||
|
||||
In this scenario, the SO response is produced incrementally in parts via streaming. The caller specifies the `ResponseFormat` and consumes the response chunks as they arrive.
|
||||
Deserialization is performed after all chunks have been received.
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "HelpfulAssistant",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<PersonInfo>()
|
||||
}
|
||||
});
|
||||
|
||||
IAsyncEnumerable<AgentResponseUpdate> updates = agent.RunStreamingAsync("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
|
||||
AgentResponse response = await updates.ToAgentResponseAsync();
|
||||
|
||||
// Deserialize the complete SO result after streaming is finished
|
||||
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>(response.Text)!;
|
||||
```
|
||||
|
||||
## 2. SO usage via `RunAsync<T>` generic method
|
||||
|
||||
This approach provides a convenient way to work with structured output on a per-run basis when the target type is known at compile time and a typed instance of the result
|
||||
is required.
|
||||
|
||||
### Decision Drivers
|
||||
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
### Considered Options
|
||||
|
||||
1. `RunAsync<T>` as an instance method of `AIAgent` class delegating to virtual `RunCoreAsync<T>`
|
||||
2. `RunAsync<T>` as an extension method using feature collection
|
||||
3. `RunAsync<T>` as a method of the new `ITypedAIAgent` interface
|
||||
4. `RunAsync<T>` as an instance method of `AIAgent` class working via the new `AgentRunOptions.ResponseFormat` property
|
||||
|
||||
### 1. `RunAsync<T>` as an instance method of `AIAgent` class delegating to virtual `RunCoreAsync<T>`
|
||||
|
||||
This option adds the `RunAsync<T>` method directly to the `AIAgent` base class.
|
||||
|
||||
```csharp
|
||||
public abstract class AIAgent
|
||||
{
|
||||
public Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this.RunCoreAsync<T>(messages, session, serializerOptions, options, cancellationToken);
|
||||
|
||||
protected virtual Task<AgentResponse<T>> RunCoreAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotSupportedException($"The agent of type '{this.GetType().FullName}' does not support typed responses.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agents with native SO support override the `RunCoreAsync<T>` method to provide their implementation. If not overridden, the method throws a `NotSupportedException`.
|
||||
|
||||
Users will call the generic `RunAsync<T>` method directly on the agent:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
```
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- The `AIAgent.RunAsync<T>` method is easily discoverable.
|
||||
- Both the SO decorator and `ChatClientAgent` have compile-time access to the type `T`, allowing them to use the native `IChatClient.GetResponseAsync<T>` API, which handles primitives and collections seamlessly.
|
||||
|
||||
Cons:
|
||||
- Agents without native SO support will still expose `RunAsync<T>`, which may be misleading.
|
||||
- `ChatClientAgent` exposing `RunAsync<T>` may be misleading when the underlying chat client does not support SO.
|
||||
- All `AIAgent` decorators must override `RunCoreAsync<T>` to properly handle `RunAsync<T>` calls.
|
||||
|
||||
### 2. `RunAsync<T>` as an extension method using feature collection
|
||||
|
||||
This option uses the Agent Framework feature collection (implemented via `AgentRunOptions.AdditionalProperties`) to pass a `StructuredOutputFeature` to agents, signaling that SO is requested.
|
||||
|
||||
Agents with native SO support check for this feature. If present, they read the target type, build the schema, invoke the underlying API, and store the response back in the feature.
|
||||
```csharp
|
||||
public class StructuredOutputFeature
|
||||
{
|
||||
public StructuredOutputFeature(Type outputType)
|
||||
{
|
||||
this.OutputType = outputType;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Type OutputType { get; set; }
|
||||
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
|
||||
public AgentResponse? Response { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
The `RunAsync<T>` extension method for `AIAgent` adds this feature to the collection.
|
||||
```csharp
|
||||
public static async Task<AgentResponse<T>> RunAsync<T>(
|
||||
this AIAgent agent,
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create the structured output feature.
|
||||
StructuredOutputFeature structuredOutputFeature = new(typeof(T))
|
||||
{
|
||||
SerializerOptions = serializerOptions,
|
||||
};
|
||||
|
||||
// Register it in the feature collection.
|
||||
((options ??= new AgentRunOptions()).AdditionalProperties ??= []).Add(typeof(StructuredOutputFeature).FullName!, structuredOutputFeature);
|
||||
|
||||
var response = await agent.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (structuredOutputFeature.Response is not null)
|
||||
{
|
||||
return new StructuredOutputResponse<T>(structuredOutputFeature.Response, response, serializerOptions);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No structured output response was generated by the agent.");
|
||||
}
|
||||
```
|
||||
|
||||
Users will call the `RunAsync<T>` extension method directly on the agent:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
```
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- The `RunAsync<T>` extension method is easily discoverable.
|
||||
- The `AIAgent` public API surface remains unchanged.
|
||||
- No changes required to `AIAgent` decorators.
|
||||
|
||||
Cons:
|
||||
- Agents without native SO support will still expose `RunAsync<T>`, which may be misleading.
|
||||
- `ChatClientAgent` exposing `RunAsync<T>` may be misleading when the underlying chat client does not support SO.
|
||||
|
||||
### 3. `RunAsync<T>` as a method of the new `ITypedAIAgent` interface
|
||||
|
||||
This option defines a new `ITypedAIAgent` interface that agents with SO support implement. Agents without SO support do not implement it, allowing users to check for SO capability via interface detection.
|
||||
|
||||
The interface:
|
||||
```csharp
|
||||
public interface ITypedAIAgent
|
||||
{
|
||||
Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Agents with SO support implement this interface:
|
||||
```csharp
|
||||
public sealed partial class ChatClientAgent : AIAgent, ITypedAIAgent
|
||||
{
|
||||
public async Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, `ChatClientAgent` presents a challenge: it can work with chat clients that either support or do not support SO. Implementing the interface does not guarantee
|
||||
the underlying chat client supports SO, which undermines the core idea of using interface detection to determine SO capability.
|
||||
|
||||
Additionally, to allow users to access interface methods on decorated agents, all decorators must implement `ITypedAIAgent`. This makes it difficult for users to
|
||||
determine whether the underlying agent actually supports SO, further weakening the purpose of this approach.
|
||||
|
||||
Furthermore, users would have to probe the agent type to check if it implements the `ITypedAIAgent` interface and cast it accordingly to access the `RunAsync<T>` methods.
|
||||
This adds friction to the user experience. A `RunAsync<T>` extension method for `AIAgent` could be provided to alleviate that.
|
||||
|
||||
Given these drawbacks, this option is more complex to implement than the others without providing clear benefits.
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- Both the SO decorator and `ChatClientAgent` have compile-time access to the type `T`, allowing them to use the native `IChatClient.GetResponseAsync<T>` API, which handles primitives and collections seamlessly.
|
||||
|
||||
Cons:
|
||||
- `ChatClientAgent` implementing `ITypedAIAgent` may be misleading when the underlying chat client does not support SO.
|
||||
- All `AIAgent` decorators must implement `ITypedAIAgent` to handle `RunAsync<T>` calls.
|
||||
- Decorators implementing the interface may mislead users into thinking the underlying agent natively supports SO.
|
||||
- Agents must implement all members of `ITypedAIAgent`, not just a core method.
|
||||
- Users must check the agent type and cast to `ITypedAIAgent` to access `RunAsync<T>`.
|
||||
|
||||
### 4. `RunAsync<T>` as an instance method of `AIAgent` class working via the new `AgentRunOptions.ResponseFormat` property
|
||||
|
||||
This option adds a `ResponseFormat` property of type `ChatResponseFormat` to `AgentRunOptions`. Agents that support SO check for the presence of
|
||||
this property in the options passed to `RunAsync` to determine whether structured output is requested. If present, they use the schema from `ResponseFormat`
|
||||
to invoke the underlying API and obtain the SO response.
|
||||
|
||||
```csharp
|
||||
public class AgentRunOptions
|
||||
{
|
||||
public ChatResponseFormat? ResponseFormat { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, a generic `RunAsync<T>` method is added to `AIAgent` that initializes the `ResponseFormat` based on the type `T` and delegates to the non-generic `RunAsync`.
|
||||
|
||||
```csharp
|
||||
public abstract class AIAgent
|
||||
{
|
||||
public async Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
var responseFormat = ChatResponseFormat.ForJsonSchema<T>(serializerOptions);
|
||||
|
||||
options = options?.Clone() ?? new AgentRunOptions();
|
||||
options.ResponseFormat = responseFormat;
|
||||
|
||||
AgentResponse response = await this.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new AgentResponse<T>(response, serializerOptions);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Users call the generic `RunAsync<T>` method directly on the agent:
|
||||
|
||||
```csharp
|
||||
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
```
|
||||
|
||||
Decision drivers satisfied:
|
||||
1. Support arrays and primitives as SO types
|
||||
2. Support complex types as SO types
|
||||
3. Work with `AIAgent` decorators (e.g., `OpenTelemetryAgent`)
|
||||
4. Enable SO for all AI agents, regardless of whether they natively support it
|
||||
|
||||
Pros:
|
||||
- The `AIAgent.RunAsync<T>` method is easily discoverable.
|
||||
- No changes required to `AIAgent` decorators
|
||||
|
||||
Cons:
|
||||
- Agents without native SO support will still expose `RunAsync<T>`, which may be misleading.
|
||||
- `ChatClientAgent` exposing `RunAsync<T>` may be misleading when the underlying chat client does not support SO.
|
||||
|
||||
### Decision Table
|
||||
|
||||
| | Option 1: Instance method + RunCoreAsync<T> | Option 2: Extension method + feature collection | Option 3: ITypedAIAgent Interface | Option 4: Instance method + AgentRunOptions.ResponseFormat |
|
||||
|---|---|---|---|---|
|
||||
| Discoverability | ✅ `RunAsync<T>` easily discoverable | ✅ `RunAsync<T>` easily discoverable | ❌ Requires type check and cast | ✅ `RunAsync<T>` easily discoverable |
|
||||
| Decorator changes | ❌ All decorators must override `RunCoreAsync<T>` | ✅ No changes required | ❌ All decorators must implement `ITypedAIAgent` | ✅ No changes required to decorators |
|
||||
| Primitives/collections handling | ✅ Native support via `IChatClient.GetResponseAsync<T>` | ❌ Must wrap/unwrap internally | ✅ Native support via `IChatClient.GetResponseAsync<T>` | ❌ Must wrap/unwrap internally |
|
||||
| Misleading API exposure | ❌ Agents without SO still expose `RunAsync<T>` | ❌ Agents without SO still expose `RunAsync<T>` | ❌ Interface on `ChatClientAgent` may be misleading | ❌ Agents without SO still expose `RunAsync<T>` |
|
||||
| Implementation burden | ❌ Decorators must override method | ❌ Must handle schema wrapping | ❌ Agents must implement all interface members | ✅ Delegates to existing `RunAsync` via `ResponseFormat` |
|
||||
|
||||
## Cross-Cutting Aspects
|
||||
|
||||
1. **The `useJsonSchemaResponseFormat` parameter**: The `ChatClientAgent.RunAsync<T>` method has this parameter to enable structured output on LLMs that do not natively support it.
|
||||
It works by adding a user message like "Respond with a JSON value conforming to the following schema:" along with the JSON schema. However, this approach has not been reliable historically. The recommendation is not to carry this parameter forward, regardless of which option is chosen.
|
||||
|
||||
2. **Primitives and array types handling**: There are a few options for how primitive and array types can be handled in the Agent Framework:
|
||||
|
||||
1. **Never wrap**, regardless of whether the schema is provided via `ResponseFormat` or `RunAsync<T>`.
|
||||
- Pro: No changes needed; user has full control.
|
||||
- Pro: No issues with unwrapping in streaming scenarios.
|
||||
- Con: User must wrap manually.
|
||||
|
||||
2. **Always wrap**, regardless of whether the schema is provided via `ResponseFormat` or `RunAsync<T>`.
|
||||
- Pro: Consistent wrapping behavior; no manual wrapping needed.
|
||||
- Con: Inconsistent unwrapping behavior; it may be unexpected to have SO result wrapped when schema is provided via `ResponseFormat`.
|
||||
- Con: Impossible to know if SO result is wrapped to unwrap it in streaming scenarios.
|
||||
|
||||
3. **Wrap only for `RunAsync<T>`** and do not wrap the schema provided via `ResponseFormat`.
|
||||
- Pro: No unexpectedly wrapped result when schema is provided via `ResponseFormat`.
|
||||
- Pro: Solves the problem with unwrapping in streaming scenarios.
|
||||
|
||||
4. **User decides** whether to wrap schema provided via `ResponseFormat` using a new `wrapPrimitivesAndArrays` property of `ChatResponseFormatJson`. For SO provided via `RunAsync<T>`, AF always wraps.
|
||||
- Pro: No manual wrapping needed; just flip a switch.
|
||||
- Pro: Solves the problem with unwrapping in streaming scenarios.
|
||||
- Con: Extends the public API surface.
|
||||
|
||||
3. **Structured output for agents without native SO support**: Some AI agents in AF do not support structured output natively. This is either because it is not part of the protocol (e.g., A2A agent) or because the agents use LLMs without structured output capabilities.
|
||||
To address this gap, AF can provide the `StructuredOutputAgent` decorator. This decorator wraps any `AIAgent` and adds structured output support by obtaining the text response from the decorated agent and delegating it to a configured chat client for JSON transformation.
|
||||
|
||||
```csharp
|
||||
public class StructuredOutputAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
|
||||
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._chatClient = Throw.IfNull(chatClient);
|
||||
}
|
||||
|
||||
protected override async Task<AgentResponse<T>> RunCoreAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Run the inner agent first, to get back the text response we want to convert.
|
||||
var textResponse = await this.InnerAgent.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Invoke the chat client to transform the text output into structured data.
|
||||
ChatResponse<T> soResponse = await this._chatClient.GetResponseAsync<T>(
|
||||
messages:
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are a json expert and when provided with any text, will convert it to the requested json format."),
|
||||
new ChatMessage(ChatRole.User, textResponse.Text)
|
||||
],
|
||||
serializerOptions: serializerOptions ?? AgentJsonUtilities.DefaultOptions,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new StructuredOutputAgentResponse(soResponse, textResponse);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The decorator preserves the original response from the decorated agent and surfaces it via the `OriginalResponse` property on the returned `StructuredOutputAgentResponse`.
|
||||
This allows users to access both the original unstructured response and the new structured response when using this decorator.
|
||||
```csharp
|
||||
public class StructuredOutputAgentResponse : AgentResponse
|
||||
{
|
||||
internal StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
|
||||
{
|
||||
this.OriginalResponse = agentResponse;
|
||||
}
|
||||
|
||||
public AgentResponse OriginalResponse { get; }
|
||||
}
|
||||
```
|
||||
|
||||
The decorator can be registered during the agent configuration step using the `UseStructuredOutput` extension method on `AIAgentBuilder`.
|
||||
|
||||
```csharp
|
||||
IChatClient meaiChatClient = chatClient.AsIChatClient();
|
||||
|
||||
AIAgent baseAgent = meaiChatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
|
||||
|
||||
// Register the StructuredOutputAgent decorator during agent building
|
||||
AIAgent agent = baseAgent
|
||||
.AsBuilder()
|
||||
.UseStructuredOutput(meaiChatClient)
|
||||
.Build();
|
||||
|
||||
AgentResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
|
||||
|
||||
Console.WriteLine($"Name: {response.Result.Name}");
|
||||
Console.WriteLine($"Age: {response.Result.Age}");
|
||||
Console.WriteLine($"Occupation: {response.Result.Occupation}");
|
||||
|
||||
var originalResponse = ((StructuredOutputAgentResponse)response.RawRepresentation!).OriginalResponse;
|
||||
Console.WriteLine($"Original unstructured response: {originalResponse.Text}");
|
||||
|
||||
```
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
It was decided to keep both approaches for structured output - via `ResponseFormat` and via `RunAsync<T>` since they serve different scenarios and use cases.
|
||||
|
||||
For the `RunAsync<T>` approach, option 4 was selected, which adds a generic `RunAsync<T>` method to `AIAgent` that works via the new `AgentRunOptions.ResponseFormat` property.
|
||||
This was chosen for its simplicity and because no changes are required to existing `AIAgent` decorators.
|
||||
|
||||
For cross-cutting aspects, the `useJsonSchemaResponseFormat` parameter will not be carried forward due to reliability issues.
|
||||
|
||||
For handling primitives and array types, option 3 was selected: wrap only for `RunAsync<T>` and do not wrap the schema provided via `ResponseFormat`.
|
||||
This avoids the issues described in the Approach 1 section note.
|
||||
|
||||
Finally, it was decided not to include the `StructuredOutputAgent` decorator in the framework, since the reliability of producing structured output via an additional
|
||||
LLM call may not be sufficient for all scenarios. Instead, this pattern is provided as a sample to demonstrate how structured output can be achieved for agents without native support,
|
||||
giving users a reference implementation they can adapt to their own requirements.
|
||||
@@ -1,211 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2026-02-24
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# AdditionalProperties for AIAgent and AgentSession
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The `AIAgent` base class currently exposes `Id`, `Name`, and `Description` as its core metadata properties, and `AgentSession` exposes only a `StateBag` property.
|
||||
Neither type has a mechanism for attaching arbitrary metadata, such as protocol-specific descriptors (e.g., A2A agent cards), hosting attributes, session-level tags, or custom user-defined metadata for discovery and routing.
|
||||
|
||||
Other types in the framework already carry `AdditionalProperties` — notably `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate` — all using `AdditionalPropertiesDictionary` from `Microsoft.Extensions.AI`.
|
||||
Adding a similar property to `AIAgent` and `AgentSession` would give both types a consistent, extensible metadata surface.
|
||||
|
||||
Related: [Work Item #2133](https://github.com/microsoft/agent-framework/issues/2133)
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Consistency**: Other core types (`AgentRunOptions`, `AgentResponse`, `AgentResponseUpdate`) already expose `AdditionalProperties`. `AIAgent` and `AgentSession` are the major abstractions that lack this.
|
||||
- **Extensibility**: Hosting libraries, protocol adapters (A2A, AG-UI), and discovery mechanisms need a place to attach agent-level and session-level metadata without subclassing.
|
||||
- **Simplicity**: The solution should be easy to understand and use; avoid over-engineering.
|
||||
- **Minimal breaking change**: The addition should not require changes to existing agent implementations.
|
||||
- **Clear semantics**: Users should understand what `AdditionalProperties` on an agent or session means and how it differs from `AdditionalProperties` on `AgentRunOptions`.
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Surface Area
|
||||
|
||||
- **Option A**: Public get-only property, auto-initialized (`AdditionalPropertiesDictionary AdditionalProperties { get; } = new()`) on both `AIAgent` and `AgentSession`
|
||||
- **Option B**: Public get/set nullable property (`AdditionalPropertiesDictionary? AdditionalProperties { get; set; }`) on both `AIAgent` and `AgentSession`
|
||||
- **Option C**: Constructor-injected dictionary with public get-only accessor on both `AIAgent` and `AgentSession`
|
||||
- **Option D**: External container/wrapper object — metadata lives outside `AIAgent` and `AgentSession`; no changes to the base classes
|
||||
|
||||
### Semantics
|
||||
|
||||
- **Option 1**: Metadata only — describes the agent or session; not propagated when calling `IChatClient`
|
||||
- **Option 2**: Passed down the stack — merged into `ChatOptions.AdditionalProperties` during `ChatClientAgent` runs
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
The chosen option is **Option D + Option 1**: an external container/wrapper object, used purely as metadata.
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good, because `AIAgent` and `AgentSession` remain unchanged, avoiding any increase to the core framework surface area while still enabling extensible metadata.
|
||||
- Good, because an external wrapper (owned by hosting/protocol libraries or user code, not the `AIAgent` / `AgentSession` base classes) can internally use `AdditionalPropertiesDictionary` to stay consistent with existing patterns on `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate`.
|
||||
- Good, because metadata-only semantics keep a clean separation from per-run extensibility (`AgentRunOptions.AdditionalProperties`) and avoid unexpected side effects during agent execution.
|
||||
- Good, because no additional allocation occurs on `AIAgent` or `AgentSession` when no metadata is needed; external wrappers can be created only when metadata is required.
|
||||
- Bad, because callers and libraries must manage and pass around both the agent/session instance and its associated metadata wrapper, keeping them correctly associated.
|
||||
- Bad, because different hosting or protocol layers may define their own wrapper types, which can fragment the ecosystem unless conventions are agreed upon.
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Option A — Public get-only property, auto-initialized
|
||||
|
||||
The property is always non-null and ready to use. Users add metadata after construction.
|
||||
|
||||
```csharp
|
||||
public abstract partial class AIAgent
|
||||
{
|
||||
public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
|
||||
}
|
||||
|
||||
public abstract partial class AgentSession
|
||||
{
|
||||
public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
|
||||
}
|
||||
|
||||
// Usage
|
||||
agent.AdditionalProperties["protocol"] = "A2A";
|
||||
agent.AdditionalProperties.Add<MyAgentCardInfo>(cardInfo);
|
||||
session.AdditionalProperties["tenant"] = tenantId;
|
||||
```
|
||||
|
||||
- Good, because users never encounter `null` — no defensive null checks needed.
|
||||
- Good, because the dictionary reference cannot be replaced, preventing accidental data loss.
|
||||
- Good, because it is the simplest API surface to use.
|
||||
- Neutral, because it always allocates, even when no metadata is needed. The allocation cost is negligible.
|
||||
- Bad, because it cannot be set at construction time as a single object (users must populate it post-construction).
|
||||
|
||||
### Option B — Public get/set nullable property
|
||||
|
||||
Matches the existing pattern on `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate`.
|
||||
|
||||
```csharp
|
||||
public abstract partial class AIAgent
|
||||
{
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
public abstract partial class AgentSession
|
||||
{
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
// Usage
|
||||
agent.AdditionalProperties ??= new();
|
||||
agent.AdditionalProperties["protocol"] = "A2A";
|
||||
session.AdditionalProperties ??= new();
|
||||
session.AdditionalProperties["tenant"] = tenantId;
|
||||
```
|
||||
|
||||
- Good, because it is consistent with the existing `AdditionalProperties` pattern on `AgentRunOptions` and `AgentResponse`.
|
||||
- Good, because it avoids allocation when no metadata is needed.
|
||||
- Bad, because every consumer must null-check before reading or writing.
|
||||
- Bad, because the entire dictionary can be replaced, risking accidental loss of metadata set by other components (e.g., a hosting library sets metadata, then user code replaces the dictionary).
|
||||
|
||||
### Option C — Constructor-injected with public get
|
||||
|
||||
The dictionary is provided at construction time and exposed as get-only.
|
||||
|
||||
```csharp
|
||||
public abstract partial class AIAgent
|
||||
{
|
||||
public AdditionalPropertiesDictionary AdditionalProperties { get; }
|
||||
|
||||
protected AIAgent(AdditionalPropertiesDictionary? additionalProperties = null)
|
||||
{
|
||||
this.AdditionalProperties = additionalProperties ?? new();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract partial class AgentSession
|
||||
{
|
||||
public AdditionalPropertiesDictionary AdditionalProperties { get; }
|
||||
|
||||
protected AgentSession(AdditionalPropertiesDictionary? additionalProperties = null)
|
||||
{
|
||||
this.AdditionalProperties = additionalProperties ?? new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Good, because an agent's metadata can be established before any code runs against it.
|
||||
- Bad, because `AdditionalPropertiesDictionary` has no read-only variant, so the constructor-injection pattern gives a false sense of immutability — callers can still mutate the dictionary contents after construction.
|
||||
- Bad, because it requires adding a constructor parameter to the abstract base classes, which is a source-breaking change for all existing `AIAgent` and `AgentSession` subclasses (even with a default value, it changes the constructor signature that derived classes chain to).
|
||||
- Bad, because it is more complex with little practical benefit over Option A, since post-construction mutation is equally possible.
|
||||
|
||||
### Option D — External container/wrapper object
|
||||
|
||||
Rather than adding `AdditionalProperties` to `AIAgent` or `AgentSession`, users wrap the agent or session in a container object that carries both the instance and any associated metadata. No changes to the base classes are required.
|
||||
|
||||
```csharp
|
||||
public class AgentWithMetadata
|
||||
{
|
||||
public required AIAgent Agent { get; init; }
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
public class SessionWithMetadata
|
||||
{
|
||||
public required AgentSession Session { get; init; }
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
// Usage
|
||||
var wrapper = new AgentWithMetadata
|
||||
{
|
||||
Agent = myAgent,
|
||||
AdditionalProperties = new() { ["protocol"] = "A2A" }
|
||||
};
|
||||
```
|
||||
|
||||
- Good, because it requires no changes to `AIAgent` or `AgentSession`, avoiding any risk of breaking existing implementations.
|
||||
- Good, because metadata is clearly external to the agent and session, eliminating any ambiguity about whether it might be passed down the execution stack.
|
||||
- Good, because the container pattern gives the user full control over the metadata lifecycle and serialization.
|
||||
- Bad, because it is not discoverable — users must know about the container convention; there is no built-in API surface guiding them.
|
||||
|
||||
### Option 1 — Metadata only
|
||||
|
||||
`AdditionalProperties` on `AIAgent` and `AgentSession` is descriptive metadata. It is **not** automatically propagated when the agent calls downstream services such as `IChatClient`.
|
||||
|
||||
- Good, because it keeps a clean separation of concerns: agent/session-level metadata vs. per-run options.
|
||||
- Good, because it avoids unintended side effects — metadata added for discovery or hosting won't leak into LLM requests.
|
||||
- Good, because per-run extensibility is already served by `AgentRunOptions.AdditionalProperties` (see [ADR 0014](0014-feature-collections.md)), so there is no gap.
|
||||
- Neutral, because users who want to pass agent metadata to the chat client can still do so manually via `AgentRunOptions`.
|
||||
|
||||
### Option 2 — Passed down the stack
|
||||
|
||||
`AdditionalProperties` on `AIAgent` and `AgentSession` are automatically merged into `ChatOptions.AdditionalProperties` (or similar) when `ChatClientAgent` invokes the underlying `IChatClient`.
|
||||
|
||||
- Good, because it provides an automatic way to send agent-level configuration to the LLM provider.
|
||||
- Bad, because it conflates metadata (describing the agent) with operational parameters (controlling LLM behavior), leading to potential confusion.
|
||||
- Bad, because it risks leaking unrelated metadata into LLM calls (e.g., hosting tags, discovery URLs).
|
||||
- Bad, because it would be `ChatClientAgent`-specific behavior on a base-class property, creating inconsistency for non-`ChatClientAgent` implementations.
|
||||
- Bad, because it duplicates the purpose of `AgentRunOptions.AdditionalProperties`, which already serves as the per-run extensibility point for passing data down the stack.
|
||||
|
||||
## Serialization Considerations
|
||||
|
||||
`AIAgent` instances are not typically serialized, so `AdditionalProperties` on `AIAgent` does not raise serialization concerns.
|
||||
|
||||
`AgentSession` instances, however, are routinely serialized and deserialized — for example, to persist conversation state across application restarts. Adding `AdditionalProperties` to `AgentSession` introduces a serialization challenge: `AdditionalPropertiesDictionary` is a `Dictionary<string, object?>`, and `object?` values do not carry enough type information for the JSON deserializer to reconstruct the original CLR types.
|
||||
|
||||
### Default behavior — JsonElement round-tripping
|
||||
|
||||
By default, when an `AgentSession` with `AdditionalProperties` is serialized and later deserialized, any complex objects stored as values in the dictionary will be deserialized as `JsonElement` rather than their original types. This is the same behavior exhibited by `ChatMessage.AdditionalProperties` and other `AdditionalPropertiesDictionary` usages in `Microsoft.Extensions.AI`, and is the approach we will follow.
|
||||
|
||||
### Custom serialization via JsonSerializerOptions
|
||||
|
||||
`AIAgent.SerializeSessionAsync` and `AIAgent.DeserializeSessionAsync` already accept an optional `JsonSerializerOptions` parameter. Users who need strongly-typed round-tripping of `AdditionalProperties` values can supply custom options with appropriate converters or type info resolvers. This is non-trivial to implement but provides full control over deserialization behavior when needed.
|
||||
|
||||
## More Information
|
||||
|
||||
- [ADR 0014 — Feature Collections](0014-feature-collections.md) established that `AdditionalProperties` on `AgentRunOptions` serves as the per-run extensibility mechanism. The proposed agent-level and session-level properties serve a complementary, distinct purpose: static metadata describing the agent or session itself.
|
||||
- `AdditionalPropertiesDictionary` is defined in `Microsoft.Extensions.AI` and is already a dependency of `Microsoft.Agents.AI.Abstractions`. No new package references are needed.
|
||||
- Type-safe access is available via the existing `AdditionalPropertiesExtensions` helper methods (`Add<T>`, `TryGetValue<T>`, `Contains<T>`, `Remove<T>`), which use `typeof(T).FullName` as the dictionary key.
|
||||
@@ -1,163 +0,0 @@
|
||||
---
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2026-02-25
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# AgentSession serialization
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Serializing AgentSessions is done today by calling SerializeSession on the AIAgent instance and deserialization
|
||||
is done via the DeserializeSession method on the AIAgent instance.
|
||||
|
||||
This approach has some drawbacks:
|
||||
|
||||
1. It requires each AgentSession implementation to implement its own serialization logic. This can lead to inconsistencies and errors if not done correctly.
|
||||
1. It means that only one serialization format can be supported at a time. If we want to support multiple formats (e.g., JSON, XML, binary), we would need to implement separate serialization logic for each format.
|
||||
1. It is not possible to serialize and deserialize lists of AgentSessions, since each need to be handled individually.
|
||||
1. Users may not realise that they need to call these specific methods to serialize/deserialize AgentSessions.
|
||||
|
||||
The reason why this approach was chosen initially is that AgentSessions may have behaviors that are attached to them and only the agent knows what behaviors to attach.
|
||||
These behaviors also have their own state that are attached to the AgentSession.
|
||||
The behaviors may have references to SDKs or other resources that cannot be created via standard deserialization mechanisms.
|
||||
E.g. an AgentSession may have a custom ChatMessageStore that knows how to store chat history in a specific storage backend and has a reference to the SDK client for that backend.
|
||||
When deserializing the AgentSession, we need to make sure that the ChatMessageStore is created with the correct SDK client.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- A. Ability to continue to support custom behaviors (AIContextProviders / ChatHistoryProviders).
|
||||
- B. Ability to serialize and deserialize AgentSessions via standard serialization mechanisms, e.g. JsonSerializer.Serialize and JsonSerializer.Deserialize.
|
||||
- C. Ability for the caller to access custom providers.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- Option 1: Separate state from behavior, serialize state only and re-attach behavior on first usage
|
||||
- Option 2: Separate state from behavior, and only have state on AgentSession
|
||||
- Option 3: Keep the current approach of custom Serialize/Deserialize methods
|
||||
|
||||
### Option 1: Separate state from behavior, serialize state only and re-attach behavior on first usage
|
||||
|
||||
Decision Drivers satisfied: A, B and C (C only partially)
|
||||
|
||||
Have separate properties on the AgentSession for state and behavior and mark the behavior property with [JsonIgnore].
|
||||
After deserializing the AgentSession, the behavior is null and when the AgentSession is first used by the Agent, the behavior is created and attached to the AgentSession.
|
||||
|
||||
This requires polymorphic deserialization to be supported, so that the correct AgentSession subclass and the correct behavior state is created during deserialization.
|
||||
Since the implementations for AgentSessions and their behaviors are not all known at compile time, we need a way to register custom AgentSession types and their corresponding behavior types for serialization with System.Text.Json on our JsonUtilities helpers.
|
||||
|
||||
A drawback of this approach is that the AgentSession is in an incomplete state after deserialization until it is first used,
|
||||
so if a user was to call `GetService<MyBehavior>()` on the AgentSession before it is used by the Agent, it would return null.
|
||||
|
||||
Behaviors like ChatMessageStore and AIContextProviders would need to change to support taking state as input and exposing state publicly.
|
||||
|
||||
```csharp
|
||||
public class ChatClientAgentSession
|
||||
{
|
||||
...
|
||||
public ChatMessageStoreState ChatMessageStoreState { get; }
|
||||
public ChatMessageStore? ChatMessageStore { get; }
|
||||
...
|
||||
}
|
||||
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
|
||||
[JsonDerivedType(typeof(InMemoryChatMessageStoreState), nameof(InMemoryChatMessageStoreState))]
|
||||
public abstract class ChatMessageStoreState
|
||||
{
|
||||
}
|
||||
public class InMemoryChatMessageStoreState : ChatMessageStoreState
|
||||
{
|
||||
public IList<ChatMessage> Messages { get; set; } = [];
|
||||
}
|
||||
|
||||
public abstract class ChatMessageStore<TState>
|
||||
where TState : ChatMessageStoreState
|
||||
{
|
||||
...
|
||||
public abstract TState State { get; }
|
||||
...
|
||||
}
|
||||
|
||||
public sealed class InMemoryChatMessageStore : ChatMessageStore<InMemoryChatMessageStoreState>, IList<ChatMessage>
|
||||
{
|
||||
private readonly InMemoryChatMessageStoreState _state;
|
||||
|
||||
public InMemoryChatMessageStore(InMemoryChatMessageStoreState? state)
|
||||
{
|
||||
this._state = state ?? new InMemoryChatMessageStoreState();
|
||||
}
|
||||
|
||||
public override InMemoryChatMessageStoreState State => this._state;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
ChatClientAgent factories would need to change to support creating behaviors based on state:
|
||||
|
||||
```csharp
|
||||
public Func<ChatMessageStoreFactoryContext, ChatMessageStore>? ChatMessageStoreFactory { get; set; }
|
||||
|
||||
public class ChatMessageStoreFactoryContext
|
||||
{
|
||||
public ChatMessageStoreState? State { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
The run behavior of the ChatClientAgent would be as follows:
|
||||
|
||||
1. If an AgentSession is provided, check if the ChatMessageStore property is null.
|
||||
1. If it is, check if the ChatMessageStoreState property is null.
|
||||
1. If ChatMessageStoreState is null, check if there is a provided ChatMessageStoreFactory.
|
||||
1. If there is, call it with a ChatMessageStoreFactoryContext containing null State to create a default ChatMessageStore behavior, and update the AgentSession with the created behavior and its state.
|
||||
2. If there is not, create a default InMemoryChatMessageStore behavior, and update the AgentSession with the created behavior and its state.
|
||||
1. If ChatMessageStoreState is not null, check if there is a provided ChatMessageStoreFactory.
|
||||
1. If there is, call it with a ChatMessageStoreFactoryContext containing the State to create a ChatMessageStore behavior based on the state.
|
||||
2. If there is not, create an InMemoryChatMessageStore behavior based on the State.
|
||||
|
||||
### Option 2: Separate state from behavior, and only have state on AgentSession
|
||||
|
||||
Decision Drivers satisfied: A, B and C.
|
||||
|
||||
This is similar to Option 1 but instead of having a behavior property on the AgentSession, we only have a StateBag property on the AgentSession.
|
||||
Behaviors really make more sense to live with the agent rather than the Session, but state should live on the session.
|
||||
When the AgentSession is used by the Agent, the Agent runs the behaviors against the Session, and the behavior stores it's state on the Session StateBag.
|
||||
|
||||
This means that users are unable to access the behavior from the AgentSession, e.g. via `AgentSession.GetService<TBehavior>()`.
|
||||
|
||||
However, the behaviors can be public properties on the Agent or can be retrieved from the agent via `AIAgent.GetService<MyAIContextProvider>()`.
|
||||
|
||||
```csharp
|
||||
public class AgentSession
|
||||
{
|
||||
...
|
||||
public AgentSessionStateBag StateBag { get; protected set; } = new();
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: Keep the current approach of custom Serialize/Deserialize methods
|
||||
|
||||
Decision Drivers satisfied: A and C
|
||||
|
||||
This option keeps the current approach of having custom Serialize/Deserialize methods on the AgentSession and AIAgent.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option:
|
||||
|
||||
**Option 2** — separate state from behavior, with only state on the AgentSession — because it satisfies all decision drivers and provides the cleanest separation of concerns. Since not all AgentSession implementations have yet been cleanly separated from their behaviors, AIAgent.SerializeSession and AIAgent.DeserializeSession is kept for the time being, but most session types can be serialized and deserialized directly using JsonSerializer.
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good, because providers are fully stateless — the same provider instance works correctly across any number of concurrent sessions without risk of state leakage.
|
||||
- Good, because `AgentSession` can be serialized and deserialized with standard `System.Text.Json` mechanisms, satisfying decision driver B.
|
||||
- Good, because the generic `StateBag` is extensible — new providers can store arbitrary state without requiring changes to the session class.
|
||||
- Good, because users can access providers via the agent (e.g. `agent.GetService<InMemoryChatHistoryProvider>()`) satisfying decision driver C.
|
||||
- Good, because sessions are always in a complete and valid state after deserialization — there is no "incomplete until first use" problem as in Option 1.
|
||||
- Neutral, because providers cannot be accessed directly from the session; callers must go through the agent. This is a minor usability trade-off but keeps the session focused on state only.
|
||||
- Bad, because each provider must be disciplined about using `ProviderSessionState<T>` and not storing session-specific data in instance fields. This is a correctness concern for custom provider implementers.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,125 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: rogerbarreto
|
||||
date: 2026-03-06
|
||||
deciders: rogerbarreto, alliscode
|
||||
consulted: ""
|
||||
informed: ""
|
||||
---
|
||||
|
||||
# Foundry agent surface stays centered on `ChatClientAgent`
|
||||
|
||||
## Context
|
||||
|
||||
The Microsoft Foundry integration exposes two distinct usage patterns:
|
||||
|
||||
1. Direct Responses usage, where callers provide model, instructions, and tools at runtime.
|
||||
2. Server-side versioned agents, where callers create and manage `AgentVersion` resources through `AIProjectClient.Agents`.
|
||||
|
||||
We briefly explored adding public wrapper types such as `FoundryAgent`, `FoundryVersionedAgent`, and `FoundryResponsesChatClient` to make those paths feel more specialized. That direction created extra public types, duplicated existing `ChatClientAgent` behavior, and pushed samples toward compatibility helpers instead of the native Azure SDK flow.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the public surface centered on `ChatClientAgent`.
|
||||
|
||||
- Direct Responses scenarios use `AIProjectClient.AsAIAgent(...)`.
|
||||
- Server-side versioned scenarios use native `AIProjectClient.Agents` APIs to create or retrieve agent resources, then wrap `AgentRecord` or `AgentVersion` with `AIProjectClient.AsAIAgent(...)`.
|
||||
- Compatibility helpers such as `AIProjectClient.CreateAIAgentAsync(...)` and `AIProjectClient.GetAIAgentAsync(...)` remain only as obsolete migration shims.
|
||||
- Public wrapper types `FoundryAgent`, `FoundryVersionedAgent`, `FoundryResponsesChatClient`, and `FoundryResponsesChatClientAgent` are not part of the chosen direction.
|
||||
|
||||
## Why
|
||||
|
||||
- `ChatClientAgent` is already the framework abstraction used everywhere else.
|
||||
- `AIProjectClient` is the native Azure SDK entry point for versioned agent lifecycle operations.
|
||||
- A single agent abstraction avoids parallel type hierarchies for the same backend.
|
||||
- Samples become clearer when they show either:
|
||||
- direct Responses construction via `AIProjectClient.AsAIAgent(...)`, or
|
||||
- native Foundry resource management via `AIProjectClient.Agents`.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Direct Responses path
|
||||
|
||||
Use the convenience overloads on `AIProjectClient`:
|
||||
|
||||
```csharp
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
ChatClientAgent agent = aiProjectClient.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You are good at telling jokes.",
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
Or use composed `ChatClientAgent`
|
||||
|
||||
```csharp
|
||||
ProjectResponsesClient projectResponsesClient = new(new Uri(endpoint), new DefaultAzureCredential(), new AgentReference($"model:{deploymentName}"));
|
||||
|
||||
ChatClientAgent agent = new(
|
||||
chatClient: projectResponsesClient.AsIChatClient(),
|
||||
instructions: "You are good at telling jokes.",
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
This path is code-first and does not create a persistent server-side agent.
|
||||
|
||||
### Versioned agent path
|
||||
|
||||
Use the convenience overloads on `AIProjectClient`:
|
||||
|
||||
```csharp
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
"JokerAgent",
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes."
|
||||
}));
|
||||
|
||||
ChatClientAgent agent = aiProjectClient.AsAIAgent(version);
|
||||
```
|
||||
|
||||
Or use composed `ChatClientAgent`
|
||||
|
||||
```csharp
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
"JokerAgent",
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes."
|
||||
}));
|
||||
|
||||
ProjectResponsesClient projectResponsesClient = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
.GetProjectResponsesClientForAgent(new AgentReference(version.Name, version.Version));
|
||||
|
||||
ChatClientAgent agent = new(
|
||||
chatClient: projectResponsesClient.AsIChatClient(),
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
### Samples
|
||||
|
||||
- `FoundryAgents/` samples show the direct Responses path with `AIProjectClient.AsAIAgent(...)`.
|
||||
- `FoundryVersionedAgents/` samples should show native `AIProjectClient.Agents` create/get/delete flows plus `AsAIAgent(...)`.
|
||||
|
||||
### Compatibility APIs
|
||||
|
||||
Obsolete helper extensions remain only to ease migration of existing code. New samples and new guidance should not be written against them.
|
||||
|
||||
## Rejected direction
|
||||
|
||||
Do not introduce or preserve separate public wrapper types whose main purpose is to forward to `ChatClientAgent` while carrying Foundry-specific naming.
|
||||
|
||||
That approach:
|
||||
|
||||
- duplicates lifecycle concepts already present on `AIProjectClient`,
|
||||
- fragments the public API,
|
||||
- complicates samples and docs,
|
||||
- and makes migration harder by encouraging wrapper-specific affordances.
|
||||
@@ -1,960 +0,0 @@
|
||||
status: proposed
|
||||
date: 2026-03-23
|
||||
contact: sergeymenshykh
|
||||
deciders: rbarreto, westey-m, eavanvalkenburg
|
||||
---
|
||||
|
||||
# Agent Skills: Multi-Source Architecture
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The Agent Framework needs a skills system that lets agents discover and use domain-specific knowledge, reference documents, and executable scripts. Skills can originate from different sources — filesystem directories (SKILL.md files), inline C# code, or reusable class libraries — and the framework must support all three uniformly while allowing extensibility, composition, and filtering.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Skills must be definable from multiple sources: filesystem, inline code, reusable classes, etc
|
||||
- Common abstractions are needed so the provider and builder work uniformly regardless of skill origin
|
||||
- File-based scripts must support user-defined executors, enabling custom runtimes and languages; code/class-based scripts execute in-process as C# delegates
|
||||
- Skills must be filterable so consumers can include or exclude specific skills based on defined criteria
|
||||
- Multiple skill sources must be composable into a single provider
|
||||
- It must be possible to add custom skill sources (e.g., databases, REST APIs, package registries) by implementing a common abstraction
|
||||
|
||||
## Architecture
|
||||
|
||||
### Model-Facing Tools
|
||||
|
||||
Skills are presented to the model as up to three tools that progressively disclose skill content. The system prompt lists available skill names and descriptions; the model then calls these tools on demand:
|
||||
|
||||
- **`load_skill(skillName)`** — returns the full skill body (instructions, listed resources, listed scripts)
|
||||
- **`read_skill_resource(skillName, resourceName)`** — reads a supplementary resource (file-based or code-defined) associated with a skill
|
||||
- **`run_skill_script(skillName, scriptName, arguments?)`** — executes a script associated with a skill; only registered when at least one skill contains scripts
|
||||
|
||||
Each tool delegates to the corresponding method on the resolved `AgentSkill` — calling `Resource.ReadAsync()` or `Script.RunAsync()` respectively.
|
||||
|
||||
If skills have no scripts defined, the `run_skill_script` tool is **not advertised** to the model and instructions related to script execution are **not included** in the default skills instructions.
|
||||
|
||||
### Abstract Base Types
|
||||
|
||||
The architecture defines four abstract base types that all skill variants implement:
|
||||
|
||||
```csharp
|
||||
public abstract class AgentSkill
|
||||
{
|
||||
public abstract AgentSkillFrontmatter Frontmatter { get; }
|
||||
public abstract string Content { get; }
|
||||
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
|
||||
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
||||
}
|
||||
|
||||
public abstract class AgentSkillResource
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public abstract class AgentSkillScript
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public abstract class AgentSkillsSource
|
||||
{
|
||||
public abstract Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
Skill metadata is captured via `AgentSkillFrontmatter`:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentSkillFrontmatter
|
||||
{
|
||||
public AgentSkillFrontmatter(string name, string description) { ... }
|
||||
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public string? License { get; set; }
|
||||
public string? Compatibility { get; set; }
|
||||
public string? AllowedTools { get; set; }
|
||||
public AdditionalPropertiesDictionary? Metadata { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
The type hierarchy at a glance:
|
||||
|
||||
```
|
||||
AgentSkill (abstract) AgentSkillsSource (abstract)
|
||||
├── AgentFileSkill ├── AgentFileSkillsSource (public)
|
||||
└── [Programmatic] ├── AgentInMemorySkillsSource (public)
|
||||
├── AgentInlineSkill ├── AggregatingAgentSkillsSource (public)
|
||||
└── AgentClassSkill (abstract) └── DelegatingAgentSkillsSource (abstract, public)
|
||||
├── FilteringAgentSkillsSource (public)
|
||||
AgentSkillResource (abstract) ├── CachingAgentSkillsSource (public)
|
||||
├── AgentFileSkillResource └── DeduplicatingAgentSkillsSource (public)
|
||||
└── AgentInlineSkillResource
|
||||
AgentSkillScript (abstract)
|
||||
├── AgentFileSkillScript
|
||||
└── AgentInlineSkillScript
|
||||
```
|
||||
|
||||
There are two top-level categories of skills:
|
||||
|
||||
1. **File-Based Skills** — discovered from `SKILL.md` files on the filesystem. Resources and scripts are files in subdirectories.
|
||||
2. **Programmatic Skills** — defined in C# code. These are further divided into:
|
||||
- **Inline Skills** — built at runtime via the `AgentInlineSkill` class and its fluent API. Ideal for quick, agent-specific skill definitions.
|
||||
- **Class-Based Skills** — defined as reusable C# classes that subclass `AgentClassSkill`. Ideal for packaging skills as shared libraries or NuGet packages.
|
||||
|
||||
Both programmatic skill types use `AgentInlineSkillResource` and `AgentInlineSkillScript` for their resources and scripts. They are typically served by `AgentInMemorySkillsSource`, which accepts any `AgentSkill` and is not limited to programmatic skills.
|
||||
|
||||
### File-Based Skills
|
||||
|
||||
File-based skills are authored as `SKILL.md` files on disk. Resources and scripts are discovered from corresponding subfolders within the skill directory.
|
||||
|
||||
**`AgentFileSkill`** — A filesystem-based skill discovered from a directory containing a `SKILL.md` file. Parsed from YAML frontmatter; content is the raw markdown body. Resources and scripts are discovered from files in corresponding subfolders:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentFileSkill : AgentSkill
|
||||
{
|
||||
internal AgentFileSkill(
|
||||
AgentSkillFrontmatter frontmatter, string content, string path,
|
||||
IReadOnlyList<AgentSkillResource>? resources = null,
|
||||
IReadOnlyList<AgentSkillScript>? scripts = null) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentFileSkillResource`** — A file-based skill resource. Reads content from a file on disk relative to the skill directory:
|
||||
|
||||
```csharp
|
||||
internal sealed class AgentFileSkillResource : AgentSkillResource
|
||||
{
|
||||
public AgentFileSkillResource(string name, string fullPath) { ... }
|
||||
|
||||
public string FullPath { get; }
|
||||
|
||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentFileSkillScript`** — A file-based skill script that represents a script file on disk. Delegates execution to an external `AgentFileSkillScriptRunner` callback (e.g., runs Python/shell via `Process.Start`). Throws `NotSupportedException` if no executor is configured:
|
||||
|
||||
```csharp
|
||||
public delegate Task<object?> AgentFileSkillScriptRunner(
|
||||
AgentFileSkill skill, AgentFileSkillScript script,
|
||||
AIFunctionArguments arguments, CancellationToken cancellationToken);
|
||||
|
||||
public sealed class AgentFileSkillScript : AgentSkillScript
|
||||
{
|
||||
private readonly AgentFileSkillScriptRunner _executor;
|
||||
|
||||
internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
|
||||
: base(name) { ... }
|
||||
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
|
||||
{
|
||||
|
||||
return await _executor(fileSkill, this, arguments, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The executor can be provided at the **provider level** via `AgentSkillsProviderBuilder.UseFileScriptRunner(executor)` and optionally overridden for a **particular file skill** or for a **set of skills** at the file skill source level, giving fine-grained control over how different scripts are executed.
|
||||
|
||||
**`AgentFileSkillsSource`** — A skill source that discovers skills from filesystem directories containing `SKILL.md` files. Recursively scans directories (max 2 levels), validates frontmatter, and enforces path traversal and symlink security checks:
|
||||
|
||||
```csharp
|
||||
public sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
{
|
||||
public AgentFileSkillsSource(
|
||||
IEnumerable<string> skillPaths,
|
||||
AgentFileSkillScriptRunner scriptRunner,
|
||||
AgentFileSkillsSourceOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentFileSkillsSourceOptions`** — Configuration options for `AgentFileSkillsSource`. Allows customizing the allowed file extensions for resources and scripts without adding constructor parameters:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentFileSkillsSourceOptions
|
||||
{
|
||||
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
|
||||
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Example** — A file-based skill on disk and how it is added to a source:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── unit-converter/
|
||||
├── SKILL.md # frontmatter + instructions
|
||||
├── resources/
|
||||
│ └── conversion-table.csv # discovered as a resource
|
||||
└── scripts/
|
||||
└── convert.py # discovered as a script
|
||||
```
|
||||
|
||||
```csharp
|
||||
var source = new AgentFileSkillsSource(skillPaths: ["./skills"], scriptRunner: SubprocessScriptRunner.RunAsync);
|
||||
|
||||
var provider = new AgentSkillsProvider(source);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
### Programmatic Skills
|
||||
|
||||
Programmatic skills are defined in C# code rather than discovered from the filesystem. There are two kinds: **inline** and **class-based**. Both use `AgentInlineSkillResource` and `AgentInlineSkillScript` for resources and scripts, and are held by a single `AgentInMemorySkillsSource`.
|
||||
|
||||
**`AgentInMemorySkillsSource`** — A general-purpose skill source that holds any `AgentSkill` instances in memory. Although commonly used for programmatic skills (`AgentInlineSkill` and `AgentClassSkill`), it accepts any `AgentSkill` subclass and is not restricted to code-defined skills:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInMemorySkillsSource : AgentSkillsSource
|
||||
{
|
||||
public AgentInMemorySkillsSource(
|
||||
IEnumerable<AgentSkill> skills,
|
||||
ILoggerFactory? loggerFactory = null) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### Inline Skills
|
||||
|
||||
Inline skills are built at runtime via the `AgentInlineSkill` class and its fluent API. They are ideal for quick, agent-specific skill definitions where a full class hierarchy would be overkill.
|
||||
|
||||
**`AgentInlineSkill`** — A skill defined entirely in code. Resources can be static values or functions; scripts are always functions. Constructed with name, description, and instructions, then extended with resources and scripts:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkill : AgentSkill
|
||||
{
|
||||
public AgentInlineSkill(string name, string description, string instructions, string? license = null, string? compatibility = null, ...) { ... }
|
||||
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions) { ... }
|
||||
|
||||
public AgentInlineSkill AddResource(object value, string name, string? description = null);
|
||||
public AgentInlineSkill AddResource(Delegate handler, string name, string? description = null);
|
||||
public AgentInlineSkill AddScript(Delegate handler, string name, string? description = null);
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentInlineSkillResource`** — A skill resource that wraps a static value:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkillResource : AgentSkillResource
|
||||
{
|
||||
public AgentInlineSkillResource(object value, string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<object?>(_value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentInlineSkillResource`** — A skill resource backed by a delegate. The delegate is invoked via an `AIFunction` each time `ReadAsync` is called, producing a dynamic (computed) value:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkillResource : AgentSkillResource
|
||||
{
|
||||
public AgentInlineSkillResource(Delegate handler, string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
_function = AIFunctionFactory.Create(handler, name: name);
|
||||
}
|
||||
|
||||
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _function.InvokeAsync(new AIFunctionArguments() { Services = serviceProvider }, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentInlineSkillScript`** — A skill script backed by a delegate via an `AIFunction`:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkillScript : AgentSkillScript
|
||||
{
|
||||
private readonly AIFunction _function;
|
||||
|
||||
public AgentInlineSkillScript(Delegate handler, string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
_function = AIFunctionFactory.Create(handler, name: name);
|
||||
}
|
||||
|
||||
public JsonElement? ParametersSchema => _function.JsonSchema;
|
||||
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
|
||||
{
|
||||
return await _function.InvokeAsync(arguments, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example** — Creating an inline skill with a resource and script, then adding it to a source:
|
||||
|
||||
```csharp
|
||||
var skill = new AgentInlineSkill(
|
||||
name: "unit-converter",
|
||||
description: "Converts between measurement units.",
|
||||
instructions: """
|
||||
Use this skill to convert values between metric and imperial units.
|
||||
Refer to the conversion-table resource for supported unit pairs.
|
||||
Run the convert script to perform conversions.
|
||||
"""
|
||||
)
|
||||
.AddResource("kg=2.205lb, m=3.281ft, L=0.264gal", "conversion-table", "Supported unit pairs")
|
||||
.AddScript(Convert, "convert", "Converts a value between units");
|
||||
|
||||
var source = new AgentInMemorySkillsSource([skill]);
|
||||
|
||||
var provider = new AgentSkillsProvider(source);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
|
||||
static string Convert(double value, double factor)
|
||||
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
|
||||
```
|
||||
|
||||
#### Class-Based Skills
|
||||
|
||||
Class-based skills are designed for packaging skills as reusable libraries. Users subclass `AgentClassSkill` and override properties. Unlike inline skills, class-based skills are self-contained, can live in shared libraries or NuGet packages, and are well-suited for dependency injection.
|
||||
|
||||
**`AgentClassSkill`** — An abstract base class for defining skills as reusable C# classes that bundle all skill components (frontmatter, instructions, resources, scripts) together. Designed for packaging skills as distributable libraries:
|
||||
|
||||
```csharp
|
||||
public abstract class AgentClassSkill : AgentSkill
|
||||
{
|
||||
public abstract string Instructions { get; }
|
||||
|
||||
// Content is auto-synthesized from Frontmatter + Instructions + Resources + Scripts
|
||||
public override string Content =>
|
||||
SkillContentBuilder.BuildContent(Frontmatter.Name, Frontmatter.Description,
|
||||
SkillContentBuilder.BuildBody(Instructions, Resources, Scripts));
|
||||
}
|
||||
```
|
||||
|
||||
**Example** — Defining a class-based skill and adding it to a source:
|
||||
|
||||
```csharp
|
||||
public class UnitConverterSkill : AgentClassSkill
|
||||
{
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } =
|
||||
new("unit-converter", "Converts between measurement units.");
|
||||
|
||||
public override string Instructions => """
|
||||
Use this skill to convert values between metric and imperial units.
|
||||
Refer to the conversion-table resource for supported unit pairs.
|
||||
Run the convert script to perform conversions.
|
||||
""";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
|
||||
[
|
||||
new AgentInlineSkillResource("kg=2.205lb, m=3.281ft", "conversion-table"),
|
||||
];
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
|
||||
[
|
||||
new AgentInlineSkillScript(Convert, "convert"),
|
||||
];
|
||||
|
||||
private static string Convert(double value, double factor)
|
||||
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
|
||||
}
|
||||
|
||||
var source = new AgentInMemorySkillsSource([new UnitConverterSkill()]);
|
||||
|
||||
var provider = new AgentSkillsProvider(source);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
## Filtering, Caching, and Deduplication
|
||||
|
||||
The following subsections present alternative approaches for handling filtering, caching, and deduplication of skills across multiple sources.
|
||||
|
||||
### Via Composition
|
||||
|
||||
In this approach, the `AgentSkillsProvider` accepts a **single** `AgentSkillsSource`. Multiple sources are composed externally via an aggregate source, and cross-cutting concerns like filtering, caching, and deduplication are implemented as **source decorators** — subclasses of `DelegatingAgentSkillsSource` that intercept `GetSkillsAsync()`.
|
||||
|
||||
**`FilteringAgentSkillsSource`** — A decorator that applies filter logic before returning results. The decorator pattern keeps filtering orthogonal to source implementations and allows composing multiple filters:
|
||||
|
||||
```csharp
|
||||
public sealed class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
|
||||
{
|
||||
private readonly Func<AgentSkill, bool> _predicate;
|
||||
|
||||
public FilteringAgentSkillsSource(AgentSkillsSource innerSource, Func<AgentSkill, bool> predicate)
|
||||
: base(innerSource)
|
||||
{
|
||||
_predicate = predicate;
|
||||
}
|
||||
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var skills = await this.InnerSource.GetSkillsAsync(cancellationToken);
|
||||
return skills.Where(_predicate).ToList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`CachingAgentSkillsSource`** — A decorator that caches skills after the first load, keeping the provider stateless and giving consumers control over caching granularity per source. For example, file-based skills (expensive to discover) can be cached while code-defined skills remain uncached:
|
||||
|
||||
```csharp
|
||||
public sealed class CachingAgentSkillsSource : DelegatingAgentSkillsSource
|
||||
{
|
||||
private IList<AgentSkill>? _cached;
|
||||
|
||||
public CachingAgentSkillsSource(AgentSkillsSource innerSource)
|
||||
: base(innerSource)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _cached ??= await this.InnerSource.GetSkillsAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deduplication** is similarly implemented as a decorator (`DeduplicatingAgentSkillsSource`) that deduplicates by name (case-insensitive, first-one-wins) and logs a warning for skipped duplicates.
|
||||
|
||||
**Example** — Combining file-based and code-defined sources with filtering and caching:
|
||||
|
||||
```csharp
|
||||
var fileSource = new CachingAgentSkillsSource(new AgentFileSkillsSource(["./skills"]));
|
||||
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
|
||||
|
||||
var compositeSource = new FilteringAgentSkillsSource(
|
||||
new AggregatingAgentSkillsSource([fileSource, codeSource]),
|
||||
filter: s => s.Frontmatter.Name != "internal");
|
||||
|
||||
var provider = new AgentSkillsProvider(compositeSource);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Clean single-responsibility: the provider serves skills, sources provide them.
|
||||
- Caching, filtering, and deduplication are composable as source decorators — each concern is a separate, testable wrapper.
|
||||
|
||||
**Cons:**
|
||||
- DI is less flexible: multiple `AgentSkillsSource` implementations registered in the container cannot be auto-injected into the provider. The consumer must manually compose them via an aggregate source.
|
||||
- Increased public API surface: requires additional public classes (aggregate source, caching decorators, filtering decorators) that consumers need to learn and use.
|
||||
|
||||
### Via AgentSkillsProvider
|
||||
|
||||
In this approach, the `AgentSkillsProvider` accepts **`IEnumerable<AgentSkillsSource>`** and handles aggregation, filtering, caching, and deduplication internally.
|
||||
|
||||
The provider aggregates skills from all registered sources, deduplicates by name (case-insensitive, first-one-wins), caches the result after the first load, and optionally applies filtering via a predicate on `AgentSkillsProviderOptions`. Duplicate skill names are logged as warnings.
|
||||
|
||||
**Example** — Registering multiple sources directly with the provider:
|
||||
|
||||
```csharp
|
||||
// Conceptual example — in practice, use AgentSkillsProviderBuilder
|
||||
var fileSource = new AgentFileSkillsSource(["./skills"]);
|
||||
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
|
||||
|
||||
var provider = new AgentSkillsProvider(
|
||||
sources: [fileSource, codeSource],
|
||||
options: new AgentSkillsProviderOptions
|
||||
{
|
||||
Filter = s => s.Frontmatter.Name != "internal",
|
||||
});
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- DI-friendly: register multiple `AgentSkillsSource` implementations in the container, and they are all auto-injected into `AgentSkillsProvider` via `IEnumerable<AgentSkillsSource>`.
|
||||
- Smaller public API surface: no need for aggregate source, caching decorators, or filtering decorator classes — these concerns are handled internally by the provider.
|
||||
|
||||
**Cons:**
|
||||
- The provider takes on multiple responsibilities — aggregation, caching, deduplication, and filtering.
|
||||
- Less granular caching control: caching is all-or-nothing across sources rather than per-source as with decorators.
|
||||
- Less extensible: new behaviors (e.g., ordering, TTL expiration) require modifying the provider rather than adding a decorator.
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
**`AgentSkillsProviderBuilder`** provides a fluent API for composing skills from multiple sources. The builder centralizes configuration — script executors, approval callbacks, prompt templates, and filtering — so consumers don't need to know the underlying source types.
|
||||
|
||||
The builder internally decides how to wire up the object graph: it creates the appropriate source instances, applies caching and filtering, and returns a fully configured `AgentSkillsProvider`. This keeps the setup code concise while still allowing fine-grained control when needed.
|
||||
|
||||
**Example** — Using the builder to combine multiple source types with configuration:
|
||||
|
||||
```csharp
|
||||
var provider = new AgentSkillsProviderBuilder()
|
||||
.UseFileSkill("./skills") // file-based source
|
||||
.UseInlineSkills(codeSkill) // code-defined source
|
||||
.UseClassSkills(new ClassSkill()) // class-based source
|
||||
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync) // script runner
|
||||
.UseScriptApproval() // optional human-in-the-loop
|
||||
.UsePromptTemplate(customTemplate) // optional prompt customization
|
||||
.UseFilter(s => s.Frontmatter.Name != "internal") // optional skill filtering
|
||||
.Build();
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
## Adding a Custom Skill Type
|
||||
|
||||
The skills framework is designed for extensibility. While file-based and inline skills cover common
|
||||
scenarios, you can introduce entirely new skill types by subclassing the four base classes:
|
||||
|
||||
| Base class | Purpose |
|
||||
|-----------------------|-----------------------------------------------------|
|
||||
| `AgentSkillsSource` | Discovers and loads skills from a particular origin |
|
||||
| `AgentSkill` | Holds metadata, content, resources, and scripts |
|
||||
| `AgentSkillResource` | Provides supplementary content to a skill |
|
||||
| `AgentSkillScript` | Represents an executable action within a skill |
|
||||
|
||||
The example below implements a **cloud-based skill type** where skills, resources, and scripts are
|
||||
all stored in and executed through a remote cloud service (e.g., Azure Blob Storage + Azure Functions).
|
||||
|
||||
### Step 1 — Define a custom resource
|
||||
|
||||
A `CloudSkillResource` reads resource content from a cloud storage endpoint instead of the local
|
||||
filesystem:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A skill resource backed by a cloud storage endpoint.
|
||||
/// </summary>
|
||||
public sealed class CloudSkillResource : AgentSkillResource
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudSkillResource(string name, Uri blobUri, HttpClient httpClient, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
BlobUri = blobUri ?? throw new ArgumentNullException(nameof(blobUri));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI of the cloud blob that holds this resource's content.
|
||||
/// </summary>
|
||||
public Uri BlobUri { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> ReadAsync(
|
||||
IServiceProvider? serviceProvider = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _httpClient.GetStringAsync(BlobUri, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 — Define a custom script
|
||||
|
||||
A `CloudSkillScript` executes a script by calling a cloud function endpoint, passing arguments as
|
||||
the request body:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A skill script executed via a cloud function endpoint.
|
||||
/// </summary>
|
||||
public sealed class CloudSkillScript : AgentSkillScript
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudSkillScript(string name, Uri functionUri, HttpClient httpClient, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
FunctionUri = functionUri ?? throw new ArgumentNullException(nameof(functionUri));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI of the cloud function that runs this script.
|
||||
/// </summary>
|
||||
public Uri FunctionUri { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> RunAsync(
|
||||
AgentSkill skill,
|
||||
AIFunctionArguments arguments,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(arguments);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(FunctionUri, content, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 — Define a custom skill
|
||||
|
||||
A `CloudSkill` bundles cloud-specific metadata (e.g., the base endpoint) with the standard skill
|
||||
shape:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// An <see cref="AgentSkill"/> whose content, resources, and scripts are stored in a cloud service.
|
||||
/// </summary>
|
||||
public sealed class CloudSkill : AgentSkill
|
||||
{
|
||||
public CloudSkill(
|
||||
AgentSkillFrontmatter frontmatter,
|
||||
string content,
|
||||
Uri endpoint,
|
||||
IReadOnlyList<AgentSkillResource>? resources = null,
|
||||
IReadOnlyList<AgentSkillScript>? scripts = null)
|
||||
{
|
||||
Frontmatter = frontmatter ?? throw new ArgumentNullException(nameof(frontmatter));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
|
||||
Resources = resources;
|
||||
Scripts = scripts;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base cloud endpoint for this skill.
|
||||
/// </summary>
|
||||
public Uri Endpoint { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4 — Define a custom source
|
||||
|
||||
A `CloudSkillsSource` discovers skills from a cloud catalog API and constructs `CloudSkill`
|
||||
instances with their associated resources and scripts:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A skill source that discovers and loads skills from a cloud catalog API.
|
||||
/// </summary>
|
||||
public sealed class CloudSkillsSource : AgentSkillsSource
|
||||
{
|
||||
private readonly Uri _catalogUri;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudSkillsSource(Uri catalogUri, HttpClient httpClient)
|
||||
{
|
||||
_catalogUri = catalogUri ?? throw new ArgumentNullException(nameof(catalogUri));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Fetch the skill catalog from the cloud service.
|
||||
var json = await _httpClient.GetStringAsync(_catalogUri, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var catalog = JsonSerializer.Deserialize<CloudSkillCatalog>(json)!;
|
||||
|
||||
var skills = new List<AgentSkill>();
|
||||
|
||||
foreach (var entry in catalog.Skills)
|
||||
{
|
||||
var frontmatter = new AgentSkillFrontmatter(entry.Name, entry.Description);
|
||||
|
||||
// Build cloud-backed resources.
|
||||
var resources = entry.Resources
|
||||
.Select(r => new CloudSkillResource(r.Name, r.BlobUri, _httpClient, r.Description))
|
||||
.ToList<AgentSkillResource>();
|
||||
|
||||
// Build cloud-backed scripts.
|
||||
var scripts = entry.Scripts
|
||||
.Select(s => new CloudSkillScript(s.Name, s.FunctionUri, _httpClient, s.Description))
|
||||
.ToList<AgentSkillScript>();
|
||||
|
||||
skills.Add(new CloudSkill(frontmatter, entry.Content, entry.Endpoint, resources, scripts));
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5 — Register with the builder
|
||||
|
||||
Use `UseSource` to wire the custom source into the provider:
|
||||
|
||||
```csharp
|
||||
var httpClient = new HttpClient();
|
||||
|
||||
var provider = new AgentSkillsProviderBuilder()
|
||||
.UseSource(new CloudSkillsSource(
|
||||
new Uri("https://my-service.example.com/skills/catalog"),
|
||||
httpClient))
|
||||
// Mix with other source types if needed:
|
||||
.UseFileSkill("/local/skills", scriptRunner)
|
||||
.UseInlineSkills(someInlineSkill)
|
||||
.Build();
|
||||
```
|
||||
|
||||
The `AgentSkillsProvider` handles all skill types uniformly — any combination of file-based, inline,
|
||||
class-based, and custom skills can coexist in the same provider. Custom skills automatically
|
||||
participate in the model-facing tools (`load_skill`, `read_skill_resource`, `run_skill_script`),
|
||||
filtering, deduplication, and caching — no additional integration work is required.
|
||||
|
||||
## Script Representation: `AgentSkillScript` vs `AIFunction`
|
||||
|
||||
Two approaches were considered for representing executable scripts within skills:
|
||||
|
||||
### Option A — Custom `AgentSkillScript` abstract base class (original design)
|
||||
|
||||
Scripts are modeled as a custom `AgentSkillScript` abstract class with `Name`, `Description`, and
|
||||
`RunAsync(AgentSkill, AIFunctionArguments, CancellationToken)`. Concrete implementations:
|
||||
`AgentInlineSkillScript` (wraps a delegate/`AIFunction`) and `AgentFileSkillScript` (wraps a file path + executor delegate).
|
||||
|
||||
```csharp
|
||||
// Base type
|
||||
public abstract class AgentSkillScript
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
// AgentSkill exposes scripts as:
|
||||
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
||||
|
||||
// Inline script wraps an AIFunction internally
|
||||
var script = new AgentInlineSkillScript(ConvertUnits, "convert");
|
||||
|
||||
// Pre-built AIFunction must be wrapped
|
||||
var script = new AgentInlineSkillScript(myAIFunction);
|
||||
|
||||
// Class-based skill declares scripts as:
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
|
||||
[
|
||||
new AgentInlineSkillScript(ConvertUnits, "convert"),
|
||||
];
|
||||
|
||||
// Provider executes scripts by passing the owning skill:
|
||||
await script.RunAsync(skill, arguments, cancellationToken);
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Explicit skill context at execution time.** `RunAsync` receives the owning `AgentSkill`, so any script can access skill metadata or resources during execution without requiring construction-time wiring.
|
||||
- **Self-contained abstraction.** A dedicated type communicates clearly that scripts are a skills-framework concept, separate from general-purpose AI functions.
|
||||
- **Easier extensibility for custom script types.** Third-party implementations can subclass `AgentSkillScript` and access the owning skill in `RunAsync` without special setup.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Wrapper overhead.** `AgentInlineSkillScript` is a thin pass-through around `AIFunction` — it adds a class, a constructor, and an indirection layer for no behavioral difference.
|
||||
- **Parallel abstraction.** `AgentSkillScript` and `AIFunction` serve overlapping purposes (named callable with arguments), creating two parallel hierarchies for the same concept.
|
||||
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillScript` to use them as scripts, adding ceremony.
|
||||
|
||||
### Option B — Reuse `AIFunction` directly
|
||||
|
||||
Scripts are represented as `AIFunction` (from `Microsoft.Extensions.AI`). `AgentSkill.Scripts` returns
|
||||
`IReadOnlyList<AIFunction>?`. `AgentInlineSkillScript` is eliminated entirely — callers use
|
||||
`AIFunctionFactory.Create(delegate, name: ...)` or pass `AIFunction` instances directly.
|
||||
`AgentFileSkillScript` becomes an `AIFunction` subclass that captures its owning `AgentFileSkill` via
|
||||
an internal back-reference set during construction.
|
||||
|
||||
```csharp
|
||||
// AgentSkill exposes scripts as AIFunction directly:
|
||||
public abstract IReadOnlyList<AIFunction>? Scripts { get; }
|
||||
|
||||
// Inline scripts use AIFunctionFactory — no wrapper class needed
|
||||
var skill = new AgentInlineSkill("my-skill", "desc", "instructions");
|
||||
skill.AddScript(ConvertUnits, "convert"); // delegate
|
||||
skill.AddScript(myAIFunction); // pre-built AIFunction — no wrapping
|
||||
|
||||
// Class-based skill declares scripts as:
|
||||
public override IReadOnlyList<AIFunction>? Scripts { get; } =
|
||||
[
|
||||
AIFunctionFactory.Create(ConvertUnits, name: "convert"),
|
||||
];
|
||||
|
||||
// Provider executes scripts via standard AIFunction invocation:
|
||||
await script.InvokeAsync(arguments, cancellationToken);
|
||||
|
||||
// File-based scripts extend AIFunction and capture the owning skill internally:
|
||||
public sealed class AgentFileSkillScript : AIFunction
|
||||
{
|
||||
internal AgentFileSkill? Skill { get; set; } // set by AgentFileSkill constructor
|
||||
|
||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _executor(Skill!, this, arguments, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Fewer types.** Eliminates `AgentSkillScript` and `AgentInlineSkillScript`, reducing the public API surface by two classes.
|
||||
- **Seamless interop.** Any `AIFunction` — whether from `AIFunctionFactory`, a custom subclass, or an external library — can be used as a skill script with zero wrapping.
|
||||
- **Consistent with `Microsoft.Extensions.AI` ecosystem.** Scripts share the same type as tool functions used by `IChatClient` and `FunctionInvokingChatClient`, reducing conceptual overhead for developers already familiar with the ecosystem.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **No owning-skill context in invocation signature.** `AIFunction.InvokeAsync` does not accept an `AgentSkill` parameter, so `AgentFileSkillScript` must capture its owning skill via an internal setter during construction. This adds a construction-order dependency: the skill must set the back-reference on its scripts.
|
||||
- **Custom script types lose automatic skill access.** Third-party `AIFunction` subclasses that need the owning skill must implement their own mechanism (e.g., constructor injection, closure capture) instead of receiving it as a method parameter.
|
||||
- **Semantic overloading.** `AIFunction` now means both "a tool the model can call" and "a script within a skill", which could blur the distinction for framework users.
|
||||
|
||||
## Resource Representation: `AgentSkillResource` vs `AIFunction`
|
||||
|
||||
Two approaches were considered for representing skill resources (supplementary content such as references, assets, or dynamic data):
|
||||
|
||||
### Option A — Custom `AgentSkillResource` abstract base class (original design)
|
||||
|
||||
Resources are modeled as a custom `AgentSkillResource` abstract class with `Name`, `Description`, and
|
||||
`ReadAsync(IServiceProvider?, CancellationToken)`. Concrete implementations:
|
||||
`AgentInlineSkillResource` (static value, delegate, or `AIFunction` wrapper) and `AgentFileSkillResource` (reads file content from disk).
|
||||
|
||||
```csharp
|
||||
// Base type
|
||||
public abstract class AgentSkillResource
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
// AgentSkill exposes resources as:
|
||||
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
|
||||
|
||||
// Static resource
|
||||
var resource = new AgentInlineSkillResource("static content", "my-resource");
|
||||
|
||||
// Dynamic resource (delegate)
|
||||
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
|
||||
|
||||
// Pre-built AIFunction must be wrapped
|
||||
var resource = new AgentInlineSkillResource(myAIFunction);
|
||||
|
||||
// Class-based skill declares resources as:
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
|
||||
[
|
||||
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
|
||||
];
|
||||
|
||||
// Provider reads resources via:
|
||||
await resource.ReadAsync(serviceProvider, cancellationToken);
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Clear semantic distinction.** A dedicated `AgentSkillResource` type distinguishes resources (data providers) from scripts (executable actions), making the API self-documenting.
|
||||
- **Purpose-built API.** `ReadAsync` communicates intent better than `InvokeAsync` for a data-access operation.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Wrapper overhead.** `AgentInlineSkillResource` wraps `AIFunction` internally for delegate/function cases — adding a class and indirection for no behavioral difference.
|
||||
- **Parallel abstraction.** `AgentSkillResource` and `AIFunction` serve overlapping purposes (named callable that returns data), creating two parallel hierarchies.
|
||||
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillResource`, adding ceremony.
|
||||
|
||||
### Option B — Reuse `AIFunction` directly
|
||||
|
||||
Resources are represented as `AIFunction`. `AgentSkill.Resources` returns `IReadOnlyList<AIFunction>?`.
|
||||
`AgentInlineSkillResource` becomes an `AIFunction` subclass (retained as a convenience for the static-value
|
||||
pattern: `new AgentInlineSkillResource("data", "name")`). `AgentFileSkillResource` becomes an `AIFunction`
|
||||
subclass that reads file content.
|
||||
|
||||
```csharp
|
||||
// AgentSkill exposes resources as AIFunction directly:
|
||||
public abstract IReadOnlyList<AIFunction>? Resources { get; }
|
||||
|
||||
// Static resource — AgentInlineSkillResource is retained as a convenience AIFunction subclass
|
||||
var resource = new AgentInlineSkillResource("static content", "my-resource");
|
||||
|
||||
// Dynamic resource — AgentInlineSkillResource wraps delegate as AIFunction
|
||||
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
|
||||
|
||||
// Pre-built AIFunction can be used directly — no wrapping needed
|
||||
skill.AddResource(myAIFunction);
|
||||
|
||||
// Class-based skill declares resources as:
|
||||
public override IReadOnlyList<AIFunction>? Resources { get; } =
|
||||
[
|
||||
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
|
||||
];
|
||||
|
||||
// Provider reads resources via standard AIFunction invocation:
|
||||
await resource.InvokeAsync(arguments, cancellationToken);
|
||||
|
||||
// File-based resources extend AIFunction directly:
|
||||
internal sealed class AgentFileSkillResource : AIFunction
|
||||
{
|
||||
public string FullPath { get; }
|
||||
|
||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
return await File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Fewer base types.** Eliminates the `AgentSkillResource` abstract class, reducing the public API surface.
|
||||
- **Seamless interop.** Any `AIFunction` can be used as a skill resource with zero wrapping.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Loss of semantic distinction.** Resources and scripts are now both `AIFunction`, which could make it less obvious which list a function belongs to when reading code.
|
||||
- **Static values require a wrapper.** Unlike the original `ReadAsync` which could return a stored value directly, `AIFunction.InvokeAsync` implies invocation. `AgentInlineSkillResource` is retained as a convenience subclass to handle the static-value case, so this is not eliminated — just moved to a different class.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### 1. Keep `AgentSkillResource` and `AgentSkillScript` (Option A for both sections)
|
||||
|
||||
We are staying with the custom `AgentSkillResource` and `AgentSkillScript` model classes instead of reusing `AIFunction`:
|
||||
|
||||
- **Resources have no parameters.** If a consumer provides an `AIFunction` with parameters, those parameters will never be advertised to the LLM, and the resulting call will fail.
|
||||
- **Approval breaks for `AIFunction`-based representations.** When a resource or script represented by an `AIFunction` is configured with approval, the second approval invocation will not work correctly.
|
||||
- **Injecting the owning skill into an `AIFunction`-based script is problematic.** Constructor injection would introduce a circular reference between the skill and the script. An internal property setter is possible but adds coupling.
|
||||
|
||||
### 2. Make all agent skill classes internal
|
||||
|
||||
All agent-skill-related classes are made `internal` to minimize the public API surface while the feature matures. We can reconsider and promote types to `public` later based on community signal.
|
||||
|
||||
This leaves two public entry points:
|
||||
|
||||
- **`AgentSkillsProvider`** — use directly when all skills come from a single source and filtering is not needed.
|
||||
- **`AgentSkillsProviderBuilder`** — use when mixing skill types or when filtering support is required.
|
||||
|
||||
### 3. Caching at provider level
|
||||
|
||||
Caching of tools and instructions is implemented inside `AgentSkillsProvider` rather than as an external decorator. Recreating tools and instructions on every provider call is wasteful, and a caching decorator sitting outside the provider would not have the information needed to cache them effectively.
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-03-20
|
||||
deciders: eavanvalkenburg, sphenry, chetantoshnival
|
||||
consulted: taochenosu, moonbox3, dmytrostruk, giles17, alliscode
|
||||
---
|
||||
|
||||
# Provider-Leading Client Design & OpenAI Package Extraction
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The `agent-framework-core` package currently bundles OpenAI and Azure OpenAI client implementations along with their dependencies (`openai`, `azure-identity`, `azure-ai-projects`, `packaging`). This makes core heavier than necessary for users who don't use OpenAI, and it conflates the core abstractions with a specific provider implementation. Additionally, the current class naming (`OpenAIResponsesClient`, `OpenAIChatClient`) is based on the underlying OpenAI API names rather than what users actually want to do, making discoverability harder for newcomers.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Lightweight core**: Core should only contain abstractions, middleware infrastructure, and telemetry — no provider-specific code or dependencies.
|
||||
- **Discoverability-first**: Import namespaces should guide users to the right client. `from agent_framework.openai import ...` should surface all OpenAI-related clients; `from agent_framework.azure import ...` should surface Foundry, Azure AI, and other Azure-specific classes.
|
||||
- **Provider-leading naming**: The primary client name should reflect the provider, not the underlying API. The Responses API is now the recommended default for OpenAI, so its client should be called `OpenAIChatClient` (not `OpenAIResponsesClient`).
|
||||
- **Clean separation of concerns**: Azure-specific deprecated wrappers belong in the azure-ai package, not in the OpenAI package.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Keep OpenAI in core**: Simpler but keeps core heavy; doesn't help discoverability.
|
||||
- **Extract OpenAI with Azure wrappers in the OpenAI package**: Keeps Azure OpenAI wrappers alongside OpenAI code, but pollutes the OpenAI package with Azure concerns.
|
||||
- **Extract OpenAI, place Azure wrappers in azure-ai**: Clean separation; the OpenAI package has zero Azure dependencies; deprecated Azure wrappers live in a single file in azure-ai for easy future deletion.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: "Extract OpenAI, place Azure wrappers in azure-ai", because it achieves the lightest core, cleanest OpenAI package, and the most maintainable deprecation path.
|
||||
|
||||
Key changes:
|
||||
|
||||
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
|
||||
2. **Class renames**: `OpenAIResponsesClient` → `OpenAIChatClient` (Responses API), `OpenAIChatClient` → `OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
|
||||
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
|
||||
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
|
||||
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
|
||||
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
|
||||
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
|
||||
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
|
||||
|
||||
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
|
||||
|
||||
The existing `AzureAIClient` combines two concerns: CRUD lifecycle management (creating/deleting agents on the service) and runtime communication (sending messages via the Responses API). The new design removes CRUD entirely — users connect to agents that already exist in Foundry.
|
||||
|
||||
**Two approaches were considered:**
|
||||
|
||||
**Option A — `FoundryAgentClient` only (public ChatClient):**
|
||||
Users compose `Agent(client=FoundryAgentClient(...), tools=[...])`. This follows the universal `Agent(client=X)` pattern used by every other provider. However, a "client" that wraps a named remote agent (with `agent_name` as a constructor param) is semantically odd — clients typically wrap a model endpoint, not a specific agent.
|
||||
|
||||
**Option B — `FoundryAgent` (Agent subclass) + private `_FoundryAgentChatClient` and public `RawFoundryAgentChatClient`:**
|
||||
Users write `FoundryAgent(agent_name="my-agent", ...)` for the common case. Internally, `FoundryAgent` creates a `_FoundryAgentChatClient` and passes it to the standard `Agent` base class. For advanced customization, users pass `client_type=RawFoundryAgentChatClient` (or a custom subclass) to control the client middleware layers. The `Agent(client=RawFoundryAgentChatClient(...))` composition pattern still works for users who prefer it.
|
||||
|
||||
**Chosen option: Option B**, because:
|
||||
- The common case (`FoundryAgent(...)`) is a single object with no boilerplate.
|
||||
- `client_type=` gives full control over client middleware without parameter duplication — the agent forwards connection params to the client internally.
|
||||
- `RawFoundryAgent(RawAgent)` and `FoundryAgent(Agent)` mirror the established `RawAgent`/`Agent` pattern.
|
||||
- Runtime validation (only `FunctionTool` allowed) lives in `RawFoundryAgentChatClient._prepare_options`, ensuring it applies regardless of how the client is used — through `FoundryAgent`, `Agent(client=...)`, or any custom composition.
|
||||
|
||||
**Public classes:**
|
||||
- `RawFoundryAgentChatClient(RawOpenAIChatClient)` — Responses API client that injects agent reference and validates tools. Extension point for custom client middleware.
|
||||
- `RawFoundryAgent(RawAgent)` — Agent without agent-level middleware/telemetry.
|
||||
- `FoundryAgent(AgentTelemetryLayer, AgentMiddlewareLayer, RawFoundryAgent)` — Recommended production agent.
|
||||
|
||||
**Internal (private):**
|
||||
- `_FoundryAgentChatClient` — Full client with function invocation, chat middleware, and telemetry layers. Created automatically by `FoundryAgent`; users customize via `client_type=RawFoundryAgentChatClient` or a custom subclass.
|
||||
|
||||
**Deprecated:**
|
||||
- `AzureAIClient` — replaced by `FoundryAgent` (which uses `FoundryAgentClient` internally).
|
||||
- `AzureAIAgentClient` — refers to V1 Agents Service API, no direct replacement.
|
||||
- `AzureAIProjectAgentProvider` — replaced by `FoundryAgent`.
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2026-03-23
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# Chat History Persistence Consistency
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When using `ChatClientAgent` with tools, the `FunctionInvokingChatClient` (FIC) loops multiple times — service call → tool execution → service call → … — before producing a final response. There are two points of discrepancy between how chat history is stored by the framework's `ChatHistoryProvider` and how the underlying AI service stores chat history (e.g., OpenAI Responses with `store=true`):
|
||||
|
||||
1. **Persistence timing**: The AI service persists messages after *each* service call within the FIC loop. The `ChatHistoryProvider` currently persists messages only once, at the *end* of the full agent run (after all FIC loop iterations complete).
|
||||
|
||||
2. **Trailing `FunctionResultContent` storage**: When tool calling is terminated mid-loop (e.g., via `FunctionInvokingChatClient` termination filters), the final response from the agent may contain `FunctionResultContent` that was never sent to a subsequent service call. The AI service never stores this trailing `FunctionResultContent`, but the `ChatHistoryProvider` currently stores all response content, including the trailing `FunctionResultContent`.
|
||||
|
||||
These discrepancies mean that a `ChatHistoryProvider`-managed conversation and a service-managed conversation can diverge in content and structure, even when processing the same interactions.
|
||||
|
||||
### Practical Impact: Resuming After Tool-Call Termination
|
||||
|
||||
Today, users of `AIAgent` get different behaviors depending on whether chat history is stored service-side or in a `ChatHistoryProvider`. This creates concrete challenges — for example, when the function call loop is terminated and the user wants to resume the conversation in a subsequent run. With service-stored history, the trailing `FunctionResultContent` is never persisted, so the last stored message is the `FunctionCallContent` from the service. With `ChatHistoryProvider`-stored history, the trailing `FunctionResultContent` *is* persisted. The user cannot know whether the last `FunctionResultContent` is in the chat history or not without inspecting the storage mechanism, making it difficult to write resumption logic that works correctly regardless of the storage backend.
|
||||
|
||||
### Relationship Between the Two Discrepancies
|
||||
|
||||
The persistence timing and `FunctionResultContent` trimming behaviors are interrelated:
|
||||
|
||||
- **Per-service-call persistence**: When messages are persisted after each individual service call, trailing `FunctionResultContent` trimming is unnecessary. If tool calling is terminated, the `FunctionResultContent` from the terminated call was never sent to a subsequent service call, so it is never persisted. The per-service-call approach naturally matches the service's behavior.
|
||||
|
||||
- **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history.
|
||||
- **B. Atomicity**: A run that fails mid-way through a multi-step tool-calling loop should not leave chat history in a partially-updated state, unless the user explicitly opts into that behavior.
|
||||
- **C. Recoverability**: For long-running tool-calling loops, it should be possible to recover intermediate progress if the process is interrupted, rather than losing all work from the current run.
|
||||
- **D. Simplicity**: The default behavior should be easy to understand and predict for most users, without requiring knowledge of the FIC loop internals.
|
||||
- **E. Flexibility**: Regardless of the chosen default, users should be able to opt into the alternative behavior.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming
|
||||
- Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Option 1: Per-run persistence with opt-in FRC trimming
|
||||
|
||||
Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as an opt-in behavior to improve consistency with service storage.
|
||||
|
||||
- Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B.
|
||||
- Good, because the mental model is simple: one run = one history update, satisfying driver D.
|
||||
- Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A.
|
||||
- Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A.
|
||||
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C.
|
||||
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
|
||||
|
||||
### Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
||||
|
||||
Introduce an optional RequirePerServiceCallChatHistoryPersistence setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
|
||||
|
||||
Settings:
|
||||
- `RequirePerServiceCallChatHistoryPersistence` = `true`
|
||||
|
||||
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A.
|
||||
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
|
||||
- Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity.
|
||||
- Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`.
|
||||
- Bad, because the mental model is more complex: a single run may produce multiple history updates, partially failing driver D.
|
||||
- Neutral, because users can opt out to per-run persistence if they prefer atomicity, satisfying driver E.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `RequirePerServiceCallChatHistoryPersistence` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
|
||||
|
||||
### Configuration Matrix
|
||||
|
||||
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `RequirePerServiceCallChatHistoryPersistence`:
|
||||
|
||||
| `UseProvidedChatClientAsIs` | `RequirePerServiceCallChatHistoryPersistence` | Behavior |
|
||||
|---|---|---|
|
||||
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. |
|
||||
| `false` | `true` | **Per-service-call persistence (simulated).** A `PerServiceCallChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
|
||||
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. |
|
||||
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `PerServiceCallChatHistoryPersistingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B.
|
||||
- Good, because the default mental model is simple: one run = one history update, satisfying driver D.
|
||||
- Good, because users who opt into `RequirePerServiceCallChatHistoryPersistence` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
|
||||
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in.
|
||||
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled.
|
||||
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`.
|
||||
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
|
||||
- Neutral, because users who want per-service-call consistency can opt in via `RequirePerServiceCallChatHistoryPersistence = true`, satisfying driver E.
|
||||
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
#### Conversation ID Consistency
|
||||
|
||||
When `RequirePerServiceCallChatHistoryPersistence` is enabled, the `PerServiceCallChatHistoryPersistingChatClient`
|
||||
decorator also updates `session.ConversationId` after each service call. This handles two scenarios:
|
||||
|
||||
1. **Framework-managed chat history** — the decorator sets a sentinel `ConversationId` on the response
|
||||
so that `FunctionInvokingChatClient` treats the conversation as service-managed (clearing accumulated
|
||||
history between iterations and not injecting duplicate `FunctionCallContent` during approval processing).
|
||||
|
||||
2. **Service-stored chat history** — when the service returns a real `ConversationId`, the decorator
|
||||
updates `session.ConversationId` immediately after each service call, rather than deferring the update
|
||||
to the end of the run. This ensures intermediate ConversationId changes are captured even if the
|
||||
process is interrupted mid-loop.
|
||||
|
||||
For some service-stored scenarios (e.g., the Conversations API with the Responses API), there is only
|
||||
one thread with one ID, so every service call returns the same ConversationId and this per-call update
|
||||
makes no practical difference. Enabling `RequirePerServiceCallChatHistoryPersistence` ensures consistent
|
||||
per-service-call behavior across all service types regardless of how they manage ConversationIds.
|
||||
|
||||
@@ -1,815 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: bentho
|
||||
date: 2026-02-27
|
||||
deciders: bentho, markwallace-microsoft, westey-m
|
||||
consulted: Pratyush Mishra, Shivam Shrivastava, Manni Arora (Centrica eval scenario)
|
||||
informed: Agent Framework team, Foundry Evals team
|
||||
---
|
||||
|
||||
# Agent Evaluation Architecture with Azure AI Foundry Integration
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Azure AI Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
|
||||
|
||||
However, using Foundry Evals with an agent-framework agent today requires significant manual effort. Developers must:
|
||||
|
||||
1. Transform agent-framework's `Message`/`Content` types into the OpenAI-style agent message schema that Foundry evaluators expect
|
||||
2. Map tool definitions from agent-framework's `FunctionTool` format to evaluator-compatible schemas
|
||||
3. Manually wire up the correct Foundry data source type (`azure_ai_traces`, `jsonl`, `azure_ai_target_completions`, etc.) depending on their scenario
|
||||
4. Handle App Insights trace ID queries, response ID collection, and eval polling
|
||||
|
||||
Additionally, evaluation is a concern that extends beyond any single provider. Developers may want to use local evaluators (LLM-as-judge, regex, keyword matching), third-party evaluation libraries, or multiple providers in combination. The architecture must support this without creating a Foundry-specific lock-in at the API level.
|
||||
|
||||
### Functional Requirements for Agent Evaluation
|
||||
|
||||
- **Single agents and workflows.** Evaluate both individual agent responses and multi-agent workflow results, with per-agent breakdown to pinpoint underperformance.
|
||||
- **One-shot and multi-turn conversations.** Capture full conversation trajectories — including tool calls and results — not just final query/response pairs.
|
||||
- **Conversation factoring.** Support splitting conversations into query/response in multiple ways (last turn, full trajectory, per-turn) because different factorings measure different things.
|
||||
- **Multiple providers, mix and match.** Run Foundry LLM-as-judge evaluators alongside fast local checks and custom evaluators on the same data, without restructuring code.
|
||||
- **Third-party extensibility.** Any evaluation library can participate by implementing the `Evaluator` protocol (Python) or `IAgentEvaluator` interface (.NET). No predetermined list of supported libraries — the protocol is intentionally simple (`evaluate(items) → results`) so that wrappers for libraries like DeepEval, RAGAS, or Promptfoo are straightforward to write.
|
||||
- **Bring your own evaluator.** Creating a custom evaluator should be as simple as writing a function.
|
||||
- **Evaluate without re-running.** Evaluate existing responses from logs or previous runs without invoking the agent again.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Zero-friction evaluation**: Developers should go from "I have an agent" to "I have eval results" with minimal code.
|
||||
- **Provider-agnostic API**: Core evaluation capabilities must not be tied to any specific provider. Provider configuration should be separate from the evaluation call.
|
||||
- **Lowest concept count**: Introduce the fewest possible new types, abstractions, and APIs for developers to learn.
|
||||
- **Leverage existing knowledge**: The framework already knows which agents exist, what tools they have, and what conversations occurred. Evals should use this automatically rather than requiring the developer to re-specify it.
|
||||
- **Foundry-native results**: When using Foundry, results should be viewable in the Foundry portal with dashboards and comparison views.
|
||||
- **Progressive disclosure**: Simple scenarios should be near-zero code. Advanced scenarios should build on the same primitives.
|
||||
- **Cross-language parity**: Design must be implementable in both Python and .NET.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Provider-specific functions** — Build Foundry-specific helper functions (`evaluate_agent()`, etc.) directly in the Azure package. All eval functions take Foundry connection parameters.
|
||||
2. **Evaluator protocol with shared orchestration** — Define a provider-agnostic `Evaluator` protocol in the base agent library (`agent_framework` in Python, `Microsoft.Agents.AI` in .NET). Orchestration functions live alongside it. Providers implement the protocol.
|
||||
3. **Full eval framework** — Build comprehensive eval infrastructure including custom evaluator definitions, scoring profiles, and reporting inside agent-framework.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Proposed option: "Evaluator protocol with shared orchestration", because it delivers the low-friction developer experience, supports multiple providers without API changes, and keeps the concept count low.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Evaluate an agent
|
||||
|
||||
The agent is invoked once per query by default. For statistically meaningful evaluation, provide multiple diverse queries. For measuring **consistency** (does the same query produce reliable results?), use `num_repetitions` to run each query N times independently:
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
evals = FoundryEvals(
|
||||
project_client=client,
|
||||
model_deployment="gpt-4o",
|
||||
evaluators=[FoundryEvals.RELEVANCE, FoundryEvals.COHERENCE],
|
||||
)
|
||||
|
||||
results = await evaluate_agent(
|
||||
agent=my_agent,
|
||||
queries=[
|
||||
"What's the weather in Seattle?",
|
||||
"Plan a weekend trip to Portland",
|
||||
"What restaurants are near Pike Place?",
|
||||
],
|
||||
evaluators=evals,
|
||||
)
|
||||
for r in results:
|
||||
r.assert_passed()
|
||||
```
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
var evals = new FoundryEvals(chatConfiguration, FoundryEvals.Relevance, FoundryEvals.Coherence);
|
||||
|
||||
AgentEvaluationResults results = await agent.EvaluateAsync(
|
||||
new[] {
|
||||
"What's the weather in Seattle?",
|
||||
"Plan a weekend trip to Portland",
|
||||
"What restaurants are near Pike Place?",
|
||||
},
|
||||
evals);
|
||||
|
||||
results.AssertAllPassed();
|
||||
```
|
||||
|
||||
`evaluate_agent` returns one `EvalResults` per evaluator. Each result contains per-item scores with the evaluated response for auditing:
|
||||
|
||||
```
|
||||
# results[0] (FoundryEvals)
|
||||
EvalResults(status="completed", passed=3, failed=0, total=3)
|
||||
items[0]: EvalItemResult(
|
||||
query="What's the weather in Seattle?",
|
||||
response="It's currently 72°F and sunny in Seattle.",
|
||||
scores={"relevance": 5, "coherence": 5})
|
||||
items[1]: EvalItemResult(
|
||||
query="Plan a weekend trip to Portland",
|
||||
response="Here's a 2-day Portland itinerary...",
|
||||
scores={"relevance": 4, "coherence": 5})
|
||||
items[2]: EvalItemResult(
|
||||
query="What restaurants are near Pike Place?",
|
||||
response="Top restaurants near Pike Place Market: ...",
|
||||
scores={"relevance": 5, "coherence": 4})
|
||||
```
|
||||
|
||||
#### Measure consistency with repetitions
|
||||
|
||||
Run each query multiple times to detect non-deterministic behavior:
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
results = await evaluate_agent(
|
||||
agent=my_agent,
|
||||
queries=["What's the weather in Seattle?"],
|
||||
evaluators=evals,
|
||||
num_repetitions=3, # each query runs 3 times independently
|
||||
)
|
||||
# results contain 3 items (1 query × 3 repetitions)
|
||||
```
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
AgentEvaluationResults results = await agent.EvaluateAsync(
|
||||
new[] { "What's the weather in Seattle?" },
|
||||
evals,
|
||||
numRepetitions: 3); // each query runs 3 times independently
|
||||
// results contain 3 items (1 query × 3 repetitions)
|
||||
```
|
||||
|
||||
#### Evaluate a response you already have
|
||||
|
||||
When you already have agent responses, pass them directly to skip re-running the agent. Each query is paired with its corresponding response:
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
queries = ["What's the weather?", "What's the capital of France?"]
|
||||
responses = [await agent.run([Message("user", [q])]) for q in queries]
|
||||
|
||||
results = await evaluate_agent(
|
||||
responses=responses,
|
||||
evaluators=evals,
|
||||
)
|
||||
```
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
var queries = new[] { "What's the weather?" };
|
||||
var responses = new List<AgentResponse>();
|
||||
foreach (var q in queries)
|
||||
responses.Add(await agent.RunAsync(new[] { new ChatMessage(ChatRole.User, q) }));
|
||||
|
||||
AgentEvaluationResults results = await agent.EvaluateAsync(
|
||||
responses: responses,
|
||||
evals);
|
||||
```
|
||||
|
||||
Each `AgentResponse` already contains the conversation (query + response), so the evaluator extracts query/response from the conversation. When you pass `responses` without `queries`, the conversation is the source of truth.
|
||||
|
||||
#### Evaluate with conversation split strategies
|
||||
|
||||
By default, evaluators see only the last turn (final user message → final assistant response). For multi-turn conversations, you can control how the conversation is factored for evaluation:
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
results = await evaluate_agent(
|
||||
agent=agent,
|
||||
queries=["Plan a 3-day trip to Paris"],
|
||||
evaluators=evals,
|
||||
conversation_split=ConversationSplit.FULL, # evaluate entire trajectory
|
||||
)
|
||||
|
||||
# Or per-turn: each user→assistant exchange scored independently
|
||||
results = await evaluate_agent(
|
||||
agent=agent,
|
||||
queries=["Plan a 3-day trip to Paris"],
|
||||
evaluators=evals,
|
||||
conversation_split=ConversationSplit.PER_TURN,
|
||||
)
|
||||
```
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
// Full conversation as context
|
||||
AgentEvaluationResults results = await agent.EvaluateAsync(
|
||||
new[] { "Plan a 3-day trip to Paris" },
|
||||
evals,
|
||||
splitter: ConversationSplitters.Full);
|
||||
|
||||
// Per-turn splitting
|
||||
var items = EvalItem.PerTurnItems(conversation); // one EvalItem per user turn
|
||||
var results = await evals.EvaluateAsync(items);
|
||||
```
|
||||
|
||||
With `PER_TURN`, a 3-turn conversation produces 3 scored items:
|
||||
|
||||
```
|
||||
EvalResults(status="completed", passed=3, failed=0, total=3)
|
||||
items[0]: query="Plan a 3-day trip to Paris" scores={"relevance": 5}
|
||||
items[1]: query="What about restaurants?" scores={"relevance": 4}
|
||||
items[2]: query="Make it budget-friendly" scores={"relevance": 5}
|
||||
```
|
||||
|
||||
#### Evaluate a multi-agent workflow
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
result = await workflow.run("Plan a trip to Paris")
|
||||
eval_results = await evaluate_workflow(
|
||||
workflow=workflow,
|
||||
workflow_result=result,
|
||||
evaluators=evals,
|
||||
)
|
||||
|
||||
for r in eval_results:
|
||||
print(f" overall: {r.passed}/{r.total}")
|
||||
for name, sub in r.sub_results.items():
|
||||
print(f" {name}: {sub.passed}/{sub.total}")
|
||||
```
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
WorkflowRunResult result = await workflow.RunAsync("Plan a trip to Paris");
|
||||
|
||||
IReadOnlyList<AgentEvaluationResults> evalResults = await result.EvaluateAsync(evals);
|
||||
|
||||
foreach (var r in evalResults)
|
||||
{
|
||||
Console.WriteLine($" overall: {r.Passed}/{r.Total}");
|
||||
foreach (var (name, sub) in r.SubResults)
|
||||
Console.WriteLine($" {name}: {sub.Passed}/{sub.Total}");
|
||||
}
|
||||
```
|
||||
|
||||
Workflows return one result per evaluator, with sub-results per agent in the workflow:
|
||||
|
||||
```
|
||||
EvalResults(status="completed", passed=2, failed=0, total=2)
|
||||
sub_results:
|
||||
"planner": EvalResults(passed=1, total=1)
|
||||
"researcher": EvalResults(passed=1, total=1)
|
||||
```
|
||||
|
||||
#### Mix multiple providers
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
@evaluator
|
||||
def is_helpful(response: str) -> bool:
|
||||
return len(response.split()) > 10
|
||||
|
||||
foundry = FoundryEvals(
|
||||
project_client=client,
|
||||
model_deployment="gpt-4o",
|
||||
evaluators=[FoundryEvals.RELEVANCE, FoundryEvals.COHERENCE],
|
||||
)
|
||||
|
||||
results = await evaluate_agent(
|
||||
agent=agent,
|
||||
queries=queries,
|
||||
evaluators=[is_helpful, keyword_check("weather"), foundry],
|
||||
)
|
||||
```
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
IReadOnlyList<AgentEvaluationResults> results = await agent.EvaluateAsync(
|
||||
queries,
|
||||
evaluators: new IAgentEvaluator[]
|
||||
{
|
||||
new LocalEvaluator(
|
||||
EvalChecks.KeywordCheck("weather"),
|
||||
FunctionEvaluator.Create("is_helpful", (string r) => r.Split(' ').Length > 10)),
|
||||
new FoundryEvals(chatConfiguration, FoundryEvals.Relevance, FoundryEvals.Coherence),
|
||||
});
|
||||
```
|
||||
|
||||
Multiple evaluators return one result each — `results[0]` is the local evaluator, `results[1]` is Foundry.
|
||||
|
||||
#### Custom function evaluators
|
||||
|
||||
**Python:**
|
||||
|
||||
```python
|
||||
@evaluator
|
||||
def mentions_city(response: str, expected_output: str) -> bool:
|
||||
return expected_output.lower() in response.lower()
|
||||
|
||||
@evaluator
|
||||
def used_tools(conversation: list, tools: list) -> float:
|
||||
# ... scoring logic
|
||||
return score
|
||||
|
||||
local = LocalEvaluator(mentions_city, used_tools)
|
||||
```
|
||||
|
||||
`@evaluator` uses **parameter name injection** — the function's parameter names determine what data it receives from the `EvalItem`. Supported names: `query`, `response`, `expected`, `expected_tool_calls`, `conversation`, `tools`, `context`. Any combination is valid.
|
||||
|
||||
**C#:**
|
||||
|
||||
```csharp
|
||||
var local = new LocalEvaluator(
|
||||
FunctionEvaluator.Create("mentions_city",
|
||||
(EvalItem item) => item.ExpectedOutput != null
|
||||
&& item.Response.Contains(item.ExpectedOutput, StringComparison.OrdinalIgnoreCase)),
|
||||
FunctionEvaluator.Create("is_concise",
|
||||
(string response) => response.Split(' ').Length < 500));
|
||||
```
|
||||
|
||||
## What To Build
|
||||
|
||||
### Core: Evaluator Protocol
|
||||
|
||||
A runtime-checkable protocol that any evaluation provider implements:
|
||||
|
||||
```python
|
||||
@runtime_checkable
|
||||
class Evaluator(Protocol):
|
||||
name: str
|
||||
|
||||
async def evaluate(
|
||||
self, items: Sequence[EvalItem], *, eval_name: str = "Agent Framework Eval"
|
||||
) -> EvalResults: ...
|
||||
```
|
||||
|
||||
The protocol is minimal — just `name` and `evaluate()`.
|
||||
|
||||
### Core: EvalItem
|
||||
|
||||
Provider-agnostic data format for items to evaluate:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ExpectedToolCall:
|
||||
name: str # Tool/function name
|
||||
arguments: dict[str, Any] | None = None # None = don't check args
|
||||
|
||||
@dataclass
|
||||
class EvalItem:
|
||||
conversation: list[Message] # Single source of truth
|
||||
tools: list[FunctionTool] | None = None # Agent's available tools
|
||||
context: str | None = None
|
||||
expected_output: str | None = None # Ground-truth for comparison
|
||||
expected_tool_calls: list[ExpectedToolCall] | None = None
|
||||
split_strategy: ConversationSplitter | None = None
|
||||
|
||||
query: str # property — derived from conversation split
|
||||
response: str # property — derived from conversation split
|
||||
```
|
||||
|
||||
`conversation` is the single source of truth. `query` and `response` are derived properties — splitting the conversation at the last user message (default) and extracting text from each side. Changing the `split_strategy` consistently changes all derived values.
|
||||
|
||||
`tools` provides typed `FunctionTool` objects — including MCP tools, which are automatically extracted after agent runs.
|
||||
|
||||
### Internal: AgentEvalConverter
|
||||
|
||||
Internal class that converts agent-framework types to `EvalItem`. Used by `evaluate_agent()` and `evaluate_workflow()` — not part of the public API:
|
||||
|
||||
| Agent Framework | Eval Format |
|
||||
|---|---|
|
||||
| `Content.function_call` | `tool_call` in OpenAI chat format |
|
||||
| `Content.function_result` | `tool_result` in OpenAI chat format |
|
||||
| `FunctionTool` | `{name, description, parameters}` schema |
|
||||
| `Message` history | `conversation` list + `query`/`response` extraction |
|
||||
|
||||
### Core: EvalResults
|
||||
|
||||
Rich result type with convenience properties for CI integration:
|
||||
|
||||
```python
|
||||
results.all_passed # bool: no failures or errors (recursive for workflow)
|
||||
results.passed # int: passing count
|
||||
results.failed # int: failure count
|
||||
results.total # int: total = passed + failed + errored
|
||||
results.items # list[EvalItemResult]: per-item detail with query, response, and scores
|
||||
results.error # str | None: error details on failure
|
||||
results.sub_results # dict: per-agent breakdown (workflow evals)
|
||||
results.report_url # str | None: portal link (Foundry)
|
||||
results.assert_passed() # raises AssertionError with details
|
||||
```
|
||||
|
||||
### Core: Orchestration Functions
|
||||
|
||||
Provider-agnostic functions that extract data and delegate to evaluators:
|
||||
|
||||
| Function | What it does |
|
||||
|---|---|
|
||||
| `evaluate_agent()` | Runs agent against test queries (or evaluates pre-existing `responses=`), converts to `EvalItem`s, passes to evaluator. Accepts optional `expected_output=` for ground-truth comparison, `expected_tool_calls=` for tool-correctness evaluation, and `num_repetitions=` for consistency measurement |
|
||||
| `evaluate_workflow()` | Extracts per-agent data from `WorkflowRunResult`, evaluates each agent and overall output. Per-agent breakdown in `sub_results`. Also accepts `num_repetitions=` |
|
||||
|
||||
### Core: Conversation Split Strategies
|
||||
|
||||
Multi-turn conversations must be split into query (input) and response (output) halves for evaluation. How you split determines *what you're evaluating*:
|
||||
|
||||
**Last-turn split** — split at the last user message. Everything up to and including it is the query context; the agent's subsequent actions are the response:
|
||||
|
||||
```
|
||||
conversation: user1 → assistant1 → user2 → assistant2(tool) → tool_result → assistant3
|
||||
query_messages: [user1, assistant1, user2]
|
||||
response_messages: [assistant2(tool), tool_result, assistant3]
|
||||
```
|
||||
|
||||
This evaluates: "Given all the context so far, did the agent answer the latest question well?" Best for response quality at a specific point in the conversation.
|
||||
|
||||
**Full-conversation split** — the first user message is the query; everything after is the response:
|
||||
|
||||
```
|
||||
query_messages: [user1]
|
||||
response_messages: [assistant1, user2, assistant2(tool), tool_result, assistant3]
|
||||
```
|
||||
|
||||
This evaluates: "Given the original request, did the entire conversation trajectory serve the user?" Best for task completion and overall conversation quality.
|
||||
|
||||
**Per-turn split** — produces N eval items from an N-turn conversation. Each turn is evaluated with its cumulative context:
|
||||
|
||||
```
|
||||
item 1: query = [user1], response = [assistant1]
|
||||
item 2: query = [user1, assistant1, user2], response = [assistant2(tool), tool_result, assistant3]
|
||||
```
|
||||
|
||||
This evaluates each response independently. Best for fine-grained analysis and pinpointing where a conversation goes wrong.
|
||||
|
||||
These factorings produce different scores for the same conversation. The framework ships all three as built-in strategies, defaulting to last-turn. Developers can also provide a custom splitter — a function (Python) or `IConversationSplitter` implementation (.NET) — and override the strategy at the call site or per evaluator.
|
||||
|
||||
### Azure AI: FoundryEvals
|
||||
|
||||
`Evaluator` implementation backed by Azure AI Foundry:
|
||||
|
||||
```python
|
||||
class FoundryEvals:
|
||||
def __init__(self, *, project_client=None, openai_client=None,
|
||||
model_deployment: str, evaluators=None, ...)
|
||||
async def evaluate(self, items, *, eval_name) -> EvalResults
|
||||
```
|
||||
|
||||
**Smart auto-detection in `evaluate()`:**
|
||||
- Default evaluators: relevance, coherence, task_adherence
|
||||
- Auto-adds `tool_call_accuracy` when items have tools/`tool_definitions`
|
||||
- Filters out tool evaluators for items without tools
|
||||
|
||||
### Azure AI: FoundryEvals Constants
|
||||
|
||||
```python
|
||||
from agent_framework.foundry import FoundryEvals
|
||||
|
||||
evaluators = [FoundryEvals.RELEVANCE, FoundryEvals.TOOL_CALL_ACCURACY]
|
||||
```
|
||||
|
||||
Categories: Agent behavior, Tool usage, Quality, Safety.
|
||||
|
||||
### Azure AI: Foundry-Specific Functions
|
||||
|
||||
| Function | What it does |
|
||||
|---|---|
|
||||
| `evaluate_traces()` | Evaluate from stored response IDs or OTel traces |
|
||||
| `evaluate_foundry_target()` | Evaluate a Foundry-registered agent or deployment |
|
||||
|
||||
### Core: LocalEvaluator and Function Evaluators
|
||||
|
||||
`LocalEvaluator` implements the `Evaluator` protocol for fast, API-free evaluation. It runs check functions locally — useful for inner-loop development, CI smoke tests, and combining with cloud-based evaluators.
|
||||
|
||||
Built-in checks:
|
||||
- `keyword_check(*keywords)` — response must contain specified keywords
|
||||
- `tool_called_check(*tool_names)` — agent must have called specified tools
|
||||
- `tool_calls_present` — all `expected_tool_calls` names appear in conversation (unordered, extras OK)
|
||||
- `tool_call_args_match` — expected tool calls match on name + arguments (subset match on args)
|
||||
|
||||
Custom function evaluators use `@evaluator` to wrap plain Python functions. The function's **parameter names** determine what data it receives from the `EvalItem`:
|
||||
|
||||
```python
|
||||
from agent_framework import evaluator, LocalEvaluator
|
||||
|
||||
# Tier 1: Simple check — just query + response
|
||||
@evaluator
|
||||
def is_concise(response: str) -> bool:
|
||||
return len(response.split()) < 500
|
||||
|
||||
# Tier 2: Ground truth — compare against expected output
|
||||
@evaluator
|
||||
def mentions_city(response: str, expected_output: str) -> bool:
|
||||
return expected_output.lower() in response.lower()
|
||||
|
||||
# Tier 3: Full context — inspect conversation and tools
|
||||
@evaluator
|
||||
def used_tools(conversation: list, tools: list) -> float:
|
||||
# ... scoring logic
|
||||
return score
|
||||
|
||||
local = LocalEvaluator(is_concise, mentions_city, used_tools)
|
||||
```
|
||||
|
||||
Supported parameters: `query`, `response`, `expected`, `expected_tool_calls`, `conversation`, `tools`, `context`.
|
||||
Return types: `bool`, `float` (≥0.5 = pass), `dict` with `score` or `passed` key, or `CheckResult`.
|
||||
|
||||
Async functions are handled automatically — `@evaluator` detects `async def` and produces the right wrapper.
|
||||
|
||||
### Example: GAIA Benchmark
|
||||
|
||||
[GAIA](https://huggingface.co/gaia-benchmark) tests real-world multi-step tasks with known expected answers. Each task has a question and a ground-truth answer, with optional file attachments. The framework accommodates GAIA's knobs (difficulty levels, file inputs, multi-step tool use) through the existing `EvalItem` fields:
|
||||
|
||||
```python
|
||||
from datasets import load_dataset
|
||||
from agent_framework import evaluate_agent, evaluator, LocalEvaluator
|
||||
|
||||
gaia = load_dataset("gaia-benchmark/GAIA", "2023_level1", split="test")
|
||||
|
||||
@evaluator
|
||||
def exact_match(response: str, expected_output: str) -> bool:
|
||||
return expected_output.strip().lower() in response.strip().lower()
|
||||
|
||||
# Simple path — evaluate_agent handles running + expected_output stamping
|
||||
results = await evaluate_agent(
|
||||
agent=agent,
|
||||
queries=[task["Question"] for task in gaia],
|
||||
expected_output=[task["Final answer"] for task in gaia],
|
||||
evaluators=LocalEvaluator(exact_match),
|
||||
)
|
||||
```
|
||||
|
||||
### Package Location
|
||||
|
||||
- Core types and orchestration: `agent_framework._eval`, `agent_framework._local_eval` (Python), `Microsoft.Agents.AI` (.NET)
|
||||
- Foundry provider: `agent_framework_azure_ai._foundry_evals` (Python), `Microsoft.Agents.AI.AzureAI` (.NET)
|
||||
- Azure-AI re-exports core types for convenience (Python)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Tool evaluators require query + agent**: Tool evaluators need tool definition schemas. When using these evaluators with `evaluate_agent(responses=...)`, provide `queries=` and pass an agent with tool definitions.
|
||||
2. **`model_deployment` always required**: Could potentially be inferred from the Foundry project configuration.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Red teaming non-registered agents**: Requires Foundry API support for callback-based flows.
|
||||
2. **Datasets with expected outputs**: A dataset abstraction for pre-populating `expected_output` values across eval runs is a natural next step but not yet designed.
|
||||
3. **Multi-modal evaluation**: The `conversation` field on `EvalItem` already stores full `Message`/`Content` (Python) and `ChatMessage` (.NET) objects, which can represent multi-modal content (images, audio, structured data). Evaluators that accept the full `EvalItem` or `conversation` parameter can access this content today. However, the convenience shortcuts — `query`/`response` string projections and the `FunctionEvaluator` string overloads — are text-only. Multi-modal-aware evaluators should use the full-item path (`Func<EvalItem, CheckResult>` in .NET, `conversation: list` parameter in Python).
|
||||
|
||||
## .NET Implementation Design
|
||||
|
||||
### Key Difference: MEAI Ecosystem
|
||||
|
||||
Unlike Python, the .NET ecosystem already has `Microsoft.Extensions.AI.Evaluation` (v10.3.0) providing:
|
||||
|
||||
- `IEvaluator` — per-item evaluation of `(messages, chatResponse) → EvaluationResult`
|
||||
- `CompositeEvaluator` — combines multiple evaluators
|
||||
- Quality evaluators — `RelevanceEvaluator`, `CoherenceEvaluator`, `GroundednessEvaluator`
|
||||
- Safety evaluators — `ContentHarmEvaluator`, `ProtectedMaterialEvaluator`
|
||||
- Metric types — `NumericMetric`, `BooleanMetric`, `StringMetric`
|
||||
|
||||
The .NET integration uses MEAI's `IEvaluator` directly — no new evaluator interface. Our contribution is the **orchestration layer**: extension methods that run agents, extract data, call `IEvaluator` per item, and aggregate results.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Developer Code │
|
||||
│ agent.EvaluateAsync(queries, evaluator) │
|
||||
│ run.EvaluateAsync(evaluator) │
|
||||
└────────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────▼─────────────────────────────────────────────┐
|
||||
│ Orchestration Layer (Microsoft.Agents.AI) │
|
||||
│ AgentEvaluationExtensions — runs agents, extracts data, │
|
||||
│ calls IEvaluator per item, aggregates into │
|
||||
│ AgentEvaluationResults │
|
||||
└────────────────┬─────────────────────────────────────────────┘
|
||||
│ IEvaluator (MEAI)
|
||||
│
|
||||
┌───────────┼────────────┐
|
||||
│ │ │
|
||||
┌───▼───-┐ ┌───▼────┐ ┌────▼──────────┐
|
||||
│ MEAI │ │ Local │ │ Foundry │
|
||||
│ Quality│ │ Checks │ │ (cloud batch) │
|
||||
│ Safety │ │ Lambdas│ │ │
|
||||
└────────┘ └────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
All evaluators implement MEAI's `IEvaluator`. The orchestration layer doesn't need to know which kind — it calls `EvaluateAsync(messages, chatResponse)` per item on all of them. `FoundryEvals` handles batching internally (buffers items, submits once, returns per-item results).
|
||||
|
||||
### .NET Core Types
|
||||
|
||||
**No new evaluator interface.** Use MEAI's `IEvaluator` directly.
|
||||
|
||||
**`AgentEvaluationResults`** — The only new type. Aggregates per-item MEAI `EvaluationResult`s across a batch of queries:
|
||||
|
||||
```csharp
|
||||
public class AgentEvaluationResults
|
||||
{
|
||||
public string Provider { get; init; }
|
||||
public string? ReportUrl { get; init; }
|
||||
|
||||
// Per-item — standard MEAI EvaluationResult, unchanged
|
||||
public IReadOnlyList<EvaluationResult> Items { get; init; }
|
||||
|
||||
// Aggregate pass/fail derived from metric interpretations
|
||||
public int Passed { get; }
|
||||
public int Failed { get; }
|
||||
public int Total { get; }
|
||||
public bool AllPassed { get; }
|
||||
|
||||
// Workflow: per-agent breakdown
|
||||
public IReadOnlyDictionary<string, AgentEvaluationResults>? SubResults { get; init; }
|
||||
|
||||
public void AssertAllPassed(string? message = null);
|
||||
}
|
||||
```
|
||||
|
||||
### .NET Evaluator Implementations
|
||||
|
||||
All implement MEAI's `IEvaluator`:
|
||||
|
||||
**`LocalEvaluator`** — Runs lambda checks locally, returns `BooleanMetric` per check:
|
||||
|
||||
```csharp
|
||||
var local = new LocalEvaluator(
|
||||
FunctionEvaluator.Create("is_concise",
|
||||
(string response) => response.Split().Length < 500),
|
||||
EvalChecks.KeywordCheck("weather"),
|
||||
EvalChecks.ToolCalledCheck("get_weather"));
|
||||
```
|
||||
|
||||
**MEAI evaluators** — Used directly, no adapter needed:
|
||||
|
||||
```csharp
|
||||
var quality = new CompositeEvaluator(
|
||||
new RelevanceEvaluator(),
|
||||
new CoherenceEvaluator());
|
||||
```
|
||||
|
||||
**`FoundryEvals`** — Implements `IEvaluator` but batches internally. On first call, buffers the item. On the last item (or when explicitly flushed), submits the batch to Foundry and distributes per-item results:
|
||||
|
||||
```csharp
|
||||
var foundry = new FoundryEvals(projectClient, "gpt-4o");
|
||||
```
|
||||
|
||||
### .NET Orchestration: Extension Methods
|
||||
|
||||
```csharp
|
||||
public static class AgentEvaluationExtensions
|
||||
{
|
||||
// Evaluate an agent against test queries
|
||||
public static Task<AgentEvaluationResults> EvaluateAsync(
|
||||
this AIAgent agent,
|
||||
IEnumerable<string> queries,
|
||||
IEvaluator evaluator,
|
||||
ChatConfiguration? chatConfiguration = null,
|
||||
IEnumerable<string>? expectedOutput = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
// Evaluate pre-existing responses (without re-running the agent)
|
||||
public static Task<AgentEvaluationResults> EvaluateAsync(
|
||||
this AIAgent agent,
|
||||
AgentResponse responses,
|
||||
IEvaluator evaluator,
|
||||
IEnumerable<string>? queries = null,
|
||||
ChatConfiguration? chatConfiguration = null,
|
||||
IEnumerable<string>? expectedOutput = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
// Evaluate with multiple evaluators (one result per evaluator)
|
||||
public static Task<IReadOnlyList<AgentEvaluationResults>> EvaluateAsync(
|
||||
this AIAgent agent,
|
||||
IEnumerable<string> queries,
|
||||
IEnumerable<IEvaluator> evaluators,
|
||||
ChatConfiguration? chatConfiguration = null,
|
||||
IEnumerable<string>? expectedOutput = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
// Evaluate a workflow run with per-agent breakdown
|
||||
public static Task<AgentEvaluationResults> EvaluateAsync(
|
||||
this Run run,
|
||||
IEvaluator evaluator,
|
||||
ChatConfiguration? chatConfiguration = null,
|
||||
bool includeOverall = true,
|
||||
bool includePerAgent = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```csharp
|
||||
// MEAI evaluators — just works
|
||||
var results = await agent.EvaluateAsync(
|
||||
queries: ["What's the weather?"],
|
||||
evaluator: new RelevanceEvaluator(),
|
||||
chatConfiguration: new ChatConfiguration(evalClient));
|
||||
|
||||
// Local checks
|
||||
var results = await agent.EvaluateAsync(
|
||||
queries: ["What's the weather?"],
|
||||
evaluator: new LocalEvaluator(
|
||||
EvalChecks.KeywordCheck("weather")));
|
||||
|
||||
// Foundry cloud
|
||||
var results = await agent.EvaluateAsync(
|
||||
queries: ["What's the weather?"],
|
||||
evaluator: new FoundryEvals(projectClient, "gpt-4o"));
|
||||
|
||||
// Evaluate existing response (without re-running the agent)
|
||||
var response = await agent.RunAsync("What's the weather?");
|
||||
var results = await agent.EvaluateAsync(
|
||||
responses: response,
|
||||
queries: ["What's the weather?"],
|
||||
evaluator: new FoundryEvals(projectClient, "gpt-4o"));
|
||||
|
||||
// Mixed — one result per evaluator
|
||||
var results = await agent.EvaluateAsync(
|
||||
queries: ["What's the weather?"],
|
||||
evaluators: [
|
||||
new LocalEvaluator(EvalChecks.KeywordCheck("weather")),
|
||||
new RelevanceEvaluator(),
|
||||
new FoundryEvals(projectClient, "gpt-4o")
|
||||
],
|
||||
chatConfiguration: new ChatConfiguration(evalClient));
|
||||
|
||||
// Workflow with per-agent breakdown
|
||||
Run run = await workflowRunner.RunAsync(workflow, "Plan a trip");
|
||||
var results = await run.EvaluateAsync(
|
||||
evaluator: new FoundryEvals(projectClient, "gpt-4o"));
|
||||
```
|
||||
|
||||
### .NET Function Evaluators
|
||||
|
||||
Typed factory overloads (C# equivalent of Python's `@evaluator`):
|
||||
|
||||
```csharp
|
||||
public static class FunctionEvaluator
|
||||
{
|
||||
public static EvalCheck Create(string name, Func<string, bool> check); // response only
|
||||
public static EvalCheck Create(string name, Func<string, string?, bool> check); // expectedOutput
|
||||
public static EvalCheck Create(string name, Func<EvalItem, bool> check); // full item
|
||||
public static EvalCheck Create(string name, Func<EvalItem, CheckResult> check); // full control
|
||||
public static EvalCheck Create(string name, Func<string, Task<bool>> check); // async
|
||||
}
|
||||
```
|
||||
|
||||
`EvalItem` is a lightweight record used only by `FunctionEvaluator` and `LocalEvaluator` to pass context to check functions. It is not part of the `IEvaluator` interface:
|
||||
|
||||
```csharp
|
||||
public record ExpectedToolCall(string Name, IReadOnlyDictionary<string, object>? Arguments = null);
|
||||
|
||||
public sealed class EvalItem
|
||||
{
|
||||
public EvalItem(string query, string response, IReadOnlyList<ChatMessage> conversation);
|
||||
|
||||
public string Query { get; }
|
||||
public string Response { get; }
|
||||
public IReadOnlyList<ChatMessage> Conversation { get; }
|
||||
public IReadOnlyList<AITool>? Tools { get; set; }
|
||||
public string? ExpectedOutput { get; set; }
|
||||
public IReadOnlyList<ExpectedToolCall>? ExpectedToolCalls { get; set; }
|
||||
public string? Context { get; set; }
|
||||
public IConversationSplitter? Splitter { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Workflow Data Extraction (.NET)
|
||||
|
||||
`run.EvaluateAsync()` walks `Run.OutgoingEvents` via LINQ:
|
||||
|
||||
1. Pair `ExecutorInvokedEvent` / `ExecutorCompletedEvent` by `ExecutorId`
|
||||
2. Extract `AgentResponseEvent` for per-agent `ChatResponse`
|
||||
3. Call `evaluator.EvaluateAsync()` per invocation
|
||||
4. Group by `ExecutorId` for per-agent `SubResults`
|
||||
5. Use final workflow output for overall eval
|
||||
|
||||
### .NET Package Structure
|
||||
|
||||
| Package | Contents |
|
||||
|---------|----------|
|
||||
| `Microsoft.Agents.AI` | `IAgentEvaluator`, `AgentEvaluationResults`, `LocalEvaluator`, `FunctionEvaluator`, `EvalChecks`, `EvalItem`, `ExpectedToolCall`, `AgentEvaluationExtensions` |
|
||||
| `Microsoft.Agents.AI.AzureAI` | `FoundryEvals` (provider + constants) |
|
||||
|
||||
### Python ↔ .NET Mapping
|
||||
|
||||
| Python | .NET |
|
||||
|--------|------|
|
||||
| `Evaluator` protocol | `IAgentEvaluator` (our interface; MEAI provides `IEvaluator` for per-item scoring) |
|
||||
| `EvalItem` dataclass | `EvalItem` class |
|
||||
| `EvalResults` | `AgentEvaluationResults` |
|
||||
| `EvalItemResult` / `EvalScoreResult` | MEAI `EvaluationResult` / `EvaluationMetric` (reused) |
|
||||
| `LocalEvaluator` | `LocalEvaluator` (implements `IAgentEvaluator`) |
|
||||
| `@evaluator` | `FunctionEvaluator.Create()` overloads |
|
||||
| `keyword_check()` / `tool_called_check()` | `EvalChecks.KeywordCheck()` / `EvalChecks.ToolCalledCheck()` |
|
||||
| `tool_calls_present` / `tool_call_args_match` | (custom `FunctionEvaluator` — same pattern) |
|
||||
| `ExpectedToolCall` dataclass | `ExpectedToolCall` record |
|
||||
| `FoundryEvals` | `FoundryEvals` (implements `IAgentEvaluator`, includes evaluator name constants) |
|
||||
| `evaluate_agent()` | `agent.EvaluateAsync(queries, evaluator)` extension method |
|
||||
| `evaluate_agent(responses=)` | `agent.EvaluateAsync(responses, evaluator)` extension method |
|
||||
| `evaluate_workflow()` | `run.EvaluateAsync()` extension method |
|
||||
|
||||
## More Information
|
||||
|
||||
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Azure AI Foundry evaluation overview
|
||||
@@ -1,233 +0,0 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-04-07
|
||||
deciders: TBD
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# CodeAct integration through backend-specific context providers and an `execute_code` tool
|
||||
|
||||
## Introduction
|
||||
|
||||
**CodeAct** is a pattern in which the model writes executable code — rather than emitting a fixed function-call JSON schema — to plan, transform data, and orchestrate tool calls inside a single sandbox invocation. Instead of requiring a separate model round-trip for every tool call, conditional branch, or data transformation, the model produces a short program that runs in a controlled runtime, calls host-provided tools through a `call_tool(...)` bridge, and returns structured results. This reduces latency, lowers token cost, and lets the model express richer multi-step logic that is difficult to capture in a flat tool-call sequence.
|
||||
|
||||
Throughout this ADR, **CodeAct** is the primary term. **Code mode** and **programmatic tool calling** refer to the same capability.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
We need an architecture design that supports CodeAct in both Python and .NET. This is a necessary capability for the current generation of long-running agents, which need to plan, iterate, transform tool outputs, and execute bounded code inside a controlled runtime — for example, filtering a large result set, computing derived values, or chaining several tool calls with conditional logic — instead of requiring a separate model round-trip for each of those steps. The design should preserve the same behavioral contract across SDKs, but it does not need to use the same internal extension point in each runtime. We also want to standardize on Hyperlight as the initial backend, using the existing Python package and an anticipated .NET binding package once it is available.
|
||||
|
||||
Throughout this ADR, **CodeAct** is the primary term. **Code mode** and **programmatic tool calling** refer to the same capability. This ADR uses **CodeAct** consistently.
|
||||
|
||||
Model-generated code is treated as untrusted relative to the host process. This ADR assumes the selected backend provides the primary isolation boundary, while the framework is responsible for configuring approvals and capabilities, integrating telemetry, and translating outputs and failures into framework-native shapes. If a backend cannot provide isolation appropriate for its trust model, it is not a suitable CodeAct backend.
|
||||
|
||||
The core design question is: **where should CodeAct integrate into the agent pipeline so that both SDKs can offer the same functionality without invasive changes to their core function-calling loops?**
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- CodeAct must shape the model-facing surface before model invocation, not only after the model has already chosen tools.
|
||||
- The design should let users control which tools are available through CodeAct and which remain regular tools only.
|
||||
- The design must preserve existing session, approval, telemetry, and tool invocation behavior as much as possible.
|
||||
- The design should define the minimum cross-SDK telemetry and failure semantics for `execute_code`, so Python and .NET do not diverge on basic observability or error handling.
|
||||
- The design must fit naturally into the extension points that already exist in each SDK.
|
||||
- The design must be safe for concurrent runs and must not rely on mutating shared agent configuration during invocation.
|
||||
- The chosen structure should allow multiple backend-specific providers to fit under the same conceptual design over time, even though Hyperlight is the initial target.
|
||||
- The abstraction should not assume that every backend is a VM-style sandbox; alternative execution models such as Pydantic's Monty should also fit.
|
||||
- The design should allow `execute_code` to be reused both as a tool-enabled CodeAct runtime and as a standard code interpreter tool implementation.
|
||||
- The design should remain open to alternative language/runtime modes, such as JavaScript on Hyperlight, rather than baking the abstraction to Python only.
|
||||
- The design should provide a portable way to configure sandbox capabilities such as file access and network access, including allow-listed outbound domains.
|
||||
- Using CodeAct should be optional, and installing its runtime or backend dependencies should also be optional.
|
||||
- Backend-specific dependencies should be isolated behind a small adapter so SDK code is not tightly coupled to an unstable package surface.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Option 1**: Standardize on context provider-based CodeAct with a shared cross-SDK contract and backend-specific public types
|
||||
- **Option 2**: Implement CodeAct as a dedicated chat-client decorator/wrapper
|
||||
- **Option 3**: Integrate CodeAct directly into the function invocation layer/FunctionInvokingChatClient
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Option 1: Standardize on context provider-based CodeAct with a shared cross-SDK contract and backend-specific public types
|
||||
|
||||
This option uses `ContextProvider` in Python and `AIContextProvider` in .NET, but standardizes the public concept and behavior.
|
||||
In this option, the CodeAct tool set is provider-owned: only tools explicitly configured on the concrete CodeAct provider instance are available inside CodeAct, and the provider exposes direct CRUD-style management for tools, file mounts, and outbound network allow-list configuration rather than requiring a separate runtime setup object.
|
||||
The agent's direct tool surface remains separate. If a tool should be available both through CodeAct and as a normal direct tool, it is configured in both places.
|
||||
|
||||
- Good, because both SDKs already have first-class provider concepts intended for per-invocation context shaping.
|
||||
- Good, because providers operate before model invocation, which is where CodeAct must add instructions and reshape tools.
|
||||
- Good, because this lets us preserve existing function invocation behavior rather than rewriting it.
|
||||
- Good, because slightly different internals are acceptable while the public behavior remains aligned.
|
||||
- Good, because convenience builder/decorator helpers can still be added later on top of the provider model without changing the core design.
|
||||
- Good, because backend-specific runtime logic can stay inside concrete provider implementations or internal helpers instead of being forced into a lowest-common-denominator public abstraction.
|
||||
- Good, because the same provider structure can support either an all-or-nothing tool surface or a mixed side-by-side tool surface.
|
||||
- Good, because users can keep some tools direct-only while allowing other tools to be used from inside CodeAct.
|
||||
- Good, because a provider-owned CodeAct tool registry avoids mutating or inferring the agent's direct tool surface and can work consistently in both SDKs.
|
||||
- Good, because the same conceptual design can remain open to `HyperlightCodeActProvider`, a future `MontyCodeActProvider`, and other backend-specific providers over time.
|
||||
- Good, because `execute_code` can evolve into multiple backend-specific runtime modes rather than being hard-wired to one Python-plus-tools mode.
|
||||
- Bad, because the provider indirection adds per-run overhead — snapshotting the tool registry, dispatching lifecycle hooks, and building instructions — that a deeper integration point could skip. In practice this overhead is negligible relative to model inference latency and sandbox startup cost.
|
||||
|
||||
### Option 2: Implement CodeAct as a dedicated chat-client decorator/wrapper
|
||||
|
||||
This option would introduce a CodeAct-specific chat-client decorator that injects instructions and tools directly into the chat request pipeline.
|
||||
|
||||
- Good, because this is a natural fit for .NET's `DelegatingChatClient` pipeline.
|
||||
- Good, because it can also support advanced custom chat-client stacks.
|
||||
- Good, because backend-specific runtime selection could be hidden inside the decorator implementation.
|
||||
- Good, because the decorator could also encapsulate mode-specific instruction shaping for tool-enabled versus standalone interpreter behavior.
|
||||
- Good, because the decorator can decide per request whether the tool surface is exclusive or mixed.
|
||||
- Bad, because Python can support this by building a custom layering stack on top of a `Raw...Client` and swapping in a different `FunctionInvocationLayer`, but that composition path is more manual than the .NET `DelegatingChatClient` pipeline.
|
||||
- Bad, because it duplicates responsibilities already handled by provider abstractions.
|
||||
- Bad, because it makes CodeAct look more transport-specific than it really is.
|
||||
- Bad, because swappable backends and reusable interpreter or language modes become coupled to chat-client composition rather than modeled as first-class CodeAct concepts.
|
||||
|
||||
### Option 3: Integrate CodeAct directly into the function invocation layer/FunctionInvokingChatClient
|
||||
|
||||
This option would push CodeAct into Python's `FunctionInvocationLayer` and .NET's `FunctionInvokingChatClient` or related middleware.
|
||||
|
||||
- Good, because it is close to tool execution and can observe concrete tool invocation behavior.
|
||||
- Good, because function middleware may still be useful later for auxiliary auditing or policy around sandbox-originated tool calls.
|
||||
- Bad, because this is the wrong layer for constructing the model-facing tool surface and prompt instructions.
|
||||
- Bad, because it does not naturally control whether the model sees an exclusive CodeAct tool surface or a mixed side-by-side tool surface.
|
||||
- Bad, because it would still require a second mechanism for hiding normal tools and advertising `execute_code`.
|
||||
- Bad, because it is a weak fit for standalone interpreter modes where no tool-calling loop is needed.
|
||||
- Bad, because backend selection and CodeAct mode behavior are orthogonal concerns that do not belong in the function invocation layer.
|
||||
- Bad, because `.NET` would become more tightly coupled to `FunctionInvokingChatClient`, which sits below the agent framework abstraction and is not the natural cross-SDK design seam.
|
||||
|
||||
## Approval Model Options
|
||||
|
||||
- **Option A**: Bundled approval for the `execute_code` invocation
|
||||
- **Option B**: Pre-execution inspection of `call_tool(...)` references before approving `execute_code`
|
||||
- **Option C**: Nested per-tool approvals during `execute_code`
|
||||
|
||||
## Pros and Cons of the Approval Options
|
||||
|
||||
### Option A: Bundled approval for the `execute_code` invocation
|
||||
|
||||
This option grants approval once, before `execute_code` starts. Provider-owned tool calls made from inside that execution run under the same approval. The effective approval of `execute_code` is determined up front from the provider configuration rather than from inspecting which tools are actually called during execution.
|
||||
|
||||
- Good, because it is the simplest model to explain and implement consistently in both SDKs.
|
||||
- Good, because it fits naturally with long-running CodeAct loops where repeated approval interruptions would be disruptive.
|
||||
- Good, because it does not require static code analysis before execution begins.
|
||||
- Good, because it keeps the first release focused on the provider integration rather than a more complex approval engine.
|
||||
- Bad, because approval is coarse-grained and may cover more activity than the user expected.
|
||||
- Bad, because it provides less visibility into which provider-owned tools or capabilities will be exercised during the run.
|
||||
|
||||
### Option B: Pre-execution inspection of `call_tool(...)` references before approving `execute_code`
|
||||
|
||||
This option inspects submitted code for statically discoverable `call_tool("tool_name", ...)` references before execution starts and uses that information to shape the approval request.
|
||||
|
||||
- Good, because it can show users more detail up front while still keeping approval at a single pre-execution moment.
|
||||
- Good, because it matches the common case where tool names are spelled out directly in the generated code.
|
||||
- Good, because it can coexist with bundled approval as a more informative variant of the same UX.
|
||||
- Bad, because the analysis is inherently best-effort and cannot reliably predict dynamic behavior.
|
||||
- Bad, because it requires duplicated parsing or inspection logic that does not replace runtime enforcement.
|
||||
|
||||
### Option C: Nested per-tool approvals during `execute_code`
|
||||
|
||||
This option requests approval when sandboxed code actually attempts to invoke a provider-owned tool that requires approval.
|
||||
|
||||
- Good, because it aligns approval with real behavior rather than predicted behavior.
|
||||
- Good, because it gives precise visibility into which provider-owned tools are being used.
|
||||
- Good, because it can allow some tool calls while rejecting others within the same execution.
|
||||
- Bad, because it interrupts long-running CodeAct flows and can degrade the user experience significantly.
|
||||
- Bad, because it requires more complex runtime plumbing and approval UX in both SDKs.
|
||||
- Bad, because repeated approval pauses may make CodeAct less useful for the exact long-running scenarios that motivate this feature.
|
||||
|
||||
## Decision Outcomes
|
||||
|
||||
### Decision 1: Integration seam and public structure
|
||||
|
||||
Chosen option: **Option 1: Standardize on provider-based CodeAct with a shared cross-SDK contract and backend-specific public types**, because it is the only option that maps cleanly to both SDKs, lets us reshape instructions and tools before model invocation, and avoids invasive changes to the existing function invocation loops while still allowing multiple backend-specific providers and multiple runtime modes to fit under the same structure later.
|
||||
|
||||
### Decision 2: Initial approval model
|
||||
|
||||
Chosen option: **Option A: Bundled approval for the `execute_code` invocation**, because it is the smallest approval model that fits both SDKs, works well for long-running CodeAct flows, and does not force us to standardize a more complex inspection or policy engine in the first release.
|
||||
|
||||
This follows the spirit of the current Python tool approval flow, where `FunctionTool` uses `approval_mode="always_require" | "never_require"` and the auto-invocation loop escalates the whole batch when any called tool requires approval.
|
||||
|
||||
### Design summary
|
||||
|
||||
We standardize the **public concept** of CodeAct across SDKs while allowing each SDK to use the extension point that fits it best.
|
||||
|
||||
- Python uses a `ContextProvider`.
|
||||
- .NET uses an `AIContextProvider`.
|
||||
- The term **CodeAct context provider** is used throughout this ADR as a design concept, not as a required public base type. Public SDK APIs should prefer concrete backend-specific types such as `HyperlightCodeActProvider` rather than a public abstract `CodeActContextProvider` or a public `CodeActExecutor` parameter.
|
||||
- CodeAct support should ship as an optional package in each SDK rather than as part of the core package, so users who do not need CodeAct do not take on its installation and dependency footprint. That optional package may still depend on a few small, backward-compatible hooks in the host SDK's core agent pipeline.
|
||||
- There is no separate runtime setup object in the chosen design. Concrete providers manage their provider-owned CodeAct tool registry, file mounts, and outbound network allow-list configuration directly through CRUD-style methods on the provider itself.
|
||||
- At a high level, CodeAct is exposed through backend-specific context providers that contribute an `execute_code` tool, own the CodeAct-specific tool registry, and carry backend capability configuration such as filesystem and network access.
|
||||
- The initial approval model is bundled approval for `execute_code`, using the same `approval_mode="always_require" | "never_require"` vocabulary as regular tools.
|
||||
- The CodeAct provider exposes a default `approval_mode` for `execute_code`. If the provider default is `always_require`, `execute_code` is always treated as `always_require` regardless of the provider-owned tool registry. If the provider default is `never_require`, the effective approval for `execute_code` is derived from the provider-owned CodeAct tool registry captured for the run.
|
||||
- If every provider-owned CodeAct tool in that registry has `approval_mode="never_require"`, `execute_code` is treated as `never_require`. If any provider-owned CodeAct tool in that registry has `approval_mode="always_require"`, `execute_code` is treated as `always_require`, even if the generated code may not end up calling that tool.
|
||||
- Approval is granted before `execute_code` starts, and provider-owned tool calls made from inside that execution run under the same approval.
|
||||
- Direct-only agent tools do not affect the approval of `execute_code`; only the provider-owned CodeAct tool registry participates in that calculation.
|
||||
- This approval model is intentionally conservative. If one sensitive provider-owned tool forces `execute_code` to require approval more often than desired, the mitigation is to keep that tool direct-only or split it into a different provider/tool surface rather than trying to infer per-run tool usage up front.
|
||||
- Configuring filesystem and network capability state on the provider, including adding file mounts or outbound network allow-list entries, is itself the approval for those capabilities in the initial model.
|
||||
- Each `execute_code` invocation must start from a clean execution state; in-memory variables and other ephemeral interpreter/runtime state must not persist across separate calls. When a provider exposes a workspace, mounted files, or a writable artifact/output area, those files are the supported persistence mechanism across calls and are treated as external state rather than interpreter state.
|
||||
- Mutating the provider's tool registry or capability configuration while a run is in flight is allowed, but it only affects subsequent runs. Provider implementations must snapshot the effective state for each run and synchronize concurrent access so shared provider instances remain safe across concurrent runs.
|
||||
- The minimum cross-SDK telemetry contract is that `execute_code` is traced as a normal tool invocation nested inside the surrounding agent run, and provider-owned tool calls made from inside CodeAct continue to emit ordinary tool-invocation telemetry. Backend-specific resource metrics are optional extensions, not a required new top-level cross-SDK event model.
|
||||
- Timeout, out-of-memory, backend crash, and similar sandbox failures are all execution failures of `execute_code` and should surface as structured error results rather than backend-specific public DTOs. Partial textual or file outputs may be returned only when the backend can report them unambiguously; callers must not rely on partial-output recovery as a portable guarantee.
|
||||
- The provider-based structure preserves room for future pre-execution inspection and nested per-tool approvals if later experience shows they are needed.
|
||||
- Concrete backend-specific providers may still use small SDK-local helpers or adapters internally, but that split is an implementation detail rather than a public API requirement.
|
||||
|
||||
Detailed language-specific implementation notes are specified in:
|
||||
|
||||
- [Python implementation](../features/code_act/python-implementation.md)
|
||||
- [.NET implementation](../features/code_act/dotnet-implementation.md)
|
||||
|
||||
### Minimal core hooks required by the optional package
|
||||
|
||||
CodeAct remains optional at the package level, but the optional package depends on a small number of hooks that must live in the host SDK because the agent pipeline owns model invocation and per-run tool resolution.
|
||||
|
||||
- Python depends on the existing `ContextProvider` lifecycle, `SessionContext.extend_instructions(...)`, `SessionContext.extend_tools(...)`, per-run runtime tool access via `SessionContext.options["tools"]`, and the shared `ApprovalMode` vocabulary used by `FunctionTool`.
|
||||
- .NET depends on the existing `AIContextProvider` seam, agent/runtime support for applying providers before model invocation, and the existing chat-client or function-invocation seams that concrete implementations use to contribute `execute_code`.
|
||||
|
||||
These hooks are backward-compatible because they only expose or forward per-run state that core already owns. Behavior changes only when a concrete CodeAct provider opts in and uses them.
|
||||
|
||||
### Concrete provider implementation contract
|
||||
|
||||
The design does not require a public abstract `CodeActContextProvider` base class, but it does require a stable implementation contract for concrete providers.
|
||||
|
||||
- Concrete providers should expose a standard capability surface at construction time, with SDK-appropriate naming for:
|
||||
- approval mode
|
||||
- workspace root
|
||||
- file mounts
|
||||
- allowed outbound targets plus any per-target method or policy restrictions needed by the backend
|
||||
- Separate public `filesystem_mode` / `network_mode` flags are not required by the cross-SDK contract. Filesystem access may be disabled implicitly until a workspace or file mounts are configured, and outbound network may be disabled implicitly until an allow-list or equivalent outbound policy entry is configured.
|
||||
- Concrete providers should expose direct CRUD-style methods for managing the provider-owned CodeAct tool registry, file mounts, and outbound network allow-list configuration, rather than requiring callers to construct a separate runtime setup object.
|
||||
- Concrete providers should implement their host SDK's provider lifecycle hooks to:
|
||||
- build CodeAct instructions,
|
||||
- add `execute_code`,
|
||||
- snapshot the effective CodeAct tool registry and capability settings for the run,
|
||||
- compute the effective approval requirement for `execute_code`,
|
||||
- configure file access and network access for the backend,
|
||||
- prepare or restore execution state,
|
||||
- execute code,
|
||||
- and translate backend output into framework-native content.
|
||||
- Any internal abstract/helper surface shared by multiple concrete providers should standardize responsibilities for:
|
||||
- instruction construction,
|
||||
- file-access configuration,
|
||||
- network-access configuration,
|
||||
- environment preparation/restoration,
|
||||
- code execution,
|
||||
- and output-to-content conversion.
|
||||
- Backend execution output should reuse existing framework-native content/message primitives rather than introducing backend-specific public result DTOs.
|
||||
|
||||
## More Information
|
||||
|
||||
### Related artifacts
|
||||
|
||||
- Python implementation: [`docs/features/code_act/python-implementation.md`](../features/code_act/python-implementation.md)
|
||||
- .NET implementation: [`docs/features/code_act/dotnet-implementation.md`](../features/code_act/dotnet-implementation.md)
|
||||
- Python provider/session APIs: [`python/packages/core/agent_framework/_sessions.py`](../../python/packages/core/agent_framework/_sessions.py)
|
||||
- Python function invocation loop: [`python/packages/core/agent_framework/_tools.py`](../../python/packages/core/agent_framework/_tools.py)
|
||||
- .NET context provider abstraction: [`dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs`](../../dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs)
|
||||
- .NET agent integration for context providers: [`dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs`](../../dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs)
|
||||
- Optional .NET chat-client provider decorator: [`dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs`](../../dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs)
|
||||
- .NET function invocation middleware seam: [`dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgentBuilderExtensions.cs`](../../dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgentBuilderExtensions.cs)
|
||||
|
||||
### Related decisions
|
||||
|
||||
- [0015-agent-run-context](0015-agent-run-context.md)
|
||||
- [0016-python-context-middleware](0016-python-context-middleware.md)
|
||||
@@ -1,454 +0,0 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: evmattso
|
||||
date: 2026-04-10
|
||||
deciders: evmattso
|
||||
---
|
||||
|
||||
# Foundry Toolbox Support in FoundryChatClient
|
||||
|
||||
## What is the goal of this feature?
|
||||
|
||||
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in an Azure AI Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
|
||||
|
||||
A user who has configured a toolbox in the Foundry portal (or via the raw SDK) should be able to load it into an agent with a single call:
|
||||
|
||||
```python
|
||||
toolbox = await client.get_toolbox("research_tools")
|
||||
agent = Agent(client=client, instructions="...", tools=toolbox)
|
||||
```
|
||||
|
||||
**Success metric:** an agent can consume a toolbox with no manual handling of version-resolution logic on the user's side.
|
||||
|
||||
## What is the problem being solved?
|
||||
|
||||
`azure-ai-projects==2.1.0a20260409002` ships a new `BetaToolboxesOperations` surface, reachable as `AIProjectClient.beta.toolboxes` on the raw SDK client (and therefore as `FoundryChatClient.project_client.beta.toolboxes` through our wrapper), that lets teams:
|
||||
- Group related hosted tools (code interpreter, file search, MCP, web search, etc.) under a named toolbox
|
||||
- Version toolboxes immutably, so agents can pin to a specific configuration for production stability
|
||||
- Share toolboxes across multiple agents in a project
|
||||
|
||||
However, consuming a toolbox from the framework today requires:
|
||||
1. Knowing the raw SDK accessor path (`client.project_client.beta.toolboxes`)
|
||||
2. Making two calls for the common case — `.get(name)` to find the default version, then `.get_version(name, version)` to actually retrieve tools
|
||||
3. Manually unpacking `toolbox.tools` before passing them to `Agent(tools=...)`
|
||||
|
||||
None of this is hard, but it's the kind of boilerplate that should live in the client. Every other hosted tool in `FoundryChatClient` (code interpreter, file search, web search, image generation, MCP) already has a factory method (`get_code_interpreter_tool()`, etc.). Toolbox support should fit the same shape on the chat-client composition surface.
|
||||
|
||||
## API Changes
|
||||
|
||||
### One new method on the FoundryChatClient surface
|
||||
|
||||
The public toolbox-consumption surface lands on:
|
||||
|
||||
- `RawFoundryChatClient` (inherited by `FoundryChatClient`) in `_chat_client.py`
|
||||
|
||||
The implementation delegates to shared helper functions in `_tools.py` so there is a single source of truth for the SDK calls.
|
||||
|
||||
**Scope note:** `FoundryAgent` is intentionally not part of this design. `FoundryAgent` is the runtime surface for invoking an already-configured server-side Foundry agent; if that agent should use a toolbox, the toolbox/tools should already be configured on the Foundry side (UI or `azure-ai-projects` authoring flow) before MAF connects to it.
|
||||
|
||||
**Scope note:** Authoring a server-side agent whose definition references a toolbox (via `PromptAgentDefinition(tools=toolbox.tools, ...)` + `client.agents.create_version(...)`) is deliberately outside MAF scope. That is an `azure-ai-projects` / service-resource authoring concern, not a future MAF feature. Users who need it should use the raw Azure SDK directly.
|
||||
|
||||
```python
|
||||
async def get_toolbox(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
version: str | None = None,
|
||||
) -> ToolboxVersionObject:
|
||||
"""Fetch a Foundry toolbox by name.
|
||||
|
||||
If ``version`` is ``None``, resolves the toolbox's current default version
|
||||
(two requests). If ``version`` is specified, fetches that version directly
|
||||
(single request).
|
||||
|
||||
:param name: The name of the toolbox.
|
||||
:param version: Optional immutable version identifier to pin to.
|
||||
:return: A ``ToolboxVersionObject``. Pass its ``tools`` attribute to
|
||||
``Agent(tools=toolbox.tools)``.
|
||||
:raises azure.core.exceptions.ResourceNotFoundError: If the toolbox or
|
||||
version does not exist.
|
||||
"""
|
||||
|
||||
```
|
||||
|
||||
### Return types: raw SDK models, no custom wrappers
|
||||
|
||||
Methods return the `azure.ai.projects.models` types directly:
|
||||
|
||||
- `get_toolbox()` → `ToolboxVersionObject` (has `.name`, `.version`, `.tools`, `.id`, `.created_at`, `.description`, `.metadata`, `.policies`)
|
||||
|
||||
No custom wrapper classes are defined. Returning the SDK types directly:
|
||||
- Eliminates maintenance overhead of keeping a custom wrapper aligned with SDK changes
|
||||
- Matches the existing convention — `get_code_interpreter_tool()` returns the raw `CodeInterpreterTool` SDK type
|
||||
- Means any new fields the SDK adds to these types flow through automatically
|
||||
|
||||
`Agent(..., tools=...)` will accept the fetched toolbox object directly by flattening to `toolbox.tools` internally.
|
||||
|
||||
### Design decisions
|
||||
|
||||
**Instance methods, not `@staticmethod` factories.** Existing `get_code_interpreter_tool()` / `get_mcp_tool()` / etc. are `@staticmethod` because they're pure factories with no network I/O. Toolbox fetching requires the project client, so these new methods must be instance methods. This is a deliberate departure from the existing-factory pattern, justified by the async-with-I/O nature of the operation.
|
||||
|
||||
**Raw SDK type passthrough (no custom wrappers).** There is only one toolbox type in the Foundry SDK and maintaining a shadow wrapper would create alignment risk as the SDK evolves. The raw `ToolboxVersionObject` and `ToolboxObject` carry all the fields users need. Individual tools inside `toolbox.tools` are the same `azure.ai.projects.models.Tool` subclasses returned by other factory methods.
|
||||
|
||||
**Two-request default-version path.** When `version=None`, implementation calls `.get(name)` to find `default_version`, then `.get_version(name, default_version)` for the tools. Caching the default-version mapping was considered and rejected — default versions can change server-side via `update(default_version=...)`, and a stale cache would silently give callers the wrong tools. Two requests at agent setup is acceptable.
|
||||
|
||||
**No discovery/listing surface in MAF.** Discovery is intentionally left to the raw `azure-ai-projects` client. MAF does not currently expose project-resource listing surfaces for many other Foundry resources (deployments, vector stores, agents, etc.), so the toolbox design stays narrowly focused on explicit retrieval by name/version.
|
||||
|
||||
**Shared helpers in `_tools.py`.** The SDK-call helper function (`fetch_toolbox`) lives in a shared module so the chat-client surface stays thin and the request logic remains centralized.
|
||||
|
||||
**`tools=toolbox` convenience, not a new wrapper type.** Although `get_toolbox()` returns the raw `ToolboxVersionObject`, Agent Framework can still support `tools=toolbox` / `tools=[toolbox]` by flattening the toolbox's `.tools` internally. That matches existing SDK ergonomics where some higher-level objects can be placed directly in `tools=` and unpacked underneath, without introducing a public `FoundryToolbox` wrapper.
|
||||
|
||||
**Errors pass through unchanged.** `ResourceNotFoundError`, `HttpResponseError`, etc. from the SDK propagate as-is. No framework-specific exception hierarchy.
|
||||
|
||||
## E2E Code Samples
|
||||
|
||||
### Primary sample
|
||||
|
||||
New file: `samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py`
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
toolbox = await client.get_toolbox("research_tools")
|
||||
print(f"Loaded toolbox {toolbox.name}@{toolbox.version} ({len(toolbox.tools)} tools)")
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a research assistant.",
|
||||
tools=toolbox,
|
||||
)
|
||||
|
||||
result = await agent.run("What are the latest developments in quantum error correction?")
|
||||
print(f"Result: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Version pinning
|
||||
|
||||
```python
|
||||
toolbox = await client.get_toolbox("research_tools", version="v3")
|
||||
```
|
||||
|
||||
### Combining multiple toolboxes
|
||||
|
||||
```python
|
||||
toolbox_a = await client.get_toolbox("research_tools")
|
||||
toolbox_b = await client.get_toolbox("some_other_tools", version="v3")
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="...",
|
||||
tools=[toolbox_a, toolbox_b],
|
||||
)
|
||||
```
|
||||
|
||||
### Combining toolbox tools with locally defined tools
|
||||
|
||||
```python
|
||||
toolbox = await client.get_toolbox("research_tools")
|
||||
|
||||
def get_internal_metrics(metric_name: str) -> dict:
|
||||
"""Custom tool that reads from an internal dashboard."""
|
||||
...
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="...",
|
||||
tools=[get_internal_metrics, toolbox],
|
||||
)
|
||||
```
|
||||
|
||||
### Selecting only some tools from a toolbox
|
||||
|
||||
Developers will not always want to pass the entire toolbox through unchanged. A
|
||||
small helper in the Foundry package provides local post-fetch selection without
|
||||
changing the raw return type of `get_toolbox()`.
|
||||
|
||||
```python
|
||||
from agent_framework.foundry import select_toolbox_tools
|
||||
|
||||
toolbox = await client.get_toolbox("research_tools")
|
||||
|
||||
selected_tools = select_toolbox_tools(
|
||||
toolbox,
|
||||
include_names=["githubmcp", "code_interpreter"],
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="Use only the selected toolbox tools.",
|
||||
tools=selected_tools,
|
||||
)
|
||||
```
|
||||
|
||||
Supported filters:
|
||||
|
||||
```python
|
||||
from agent_framework.foundry import FoundryHostedToolType, select_toolbox_tools
|
||||
|
||||
selected_tools = select_toolbox_tools(
|
||||
toolbox,
|
||||
include_types=["mcp", "code_interpreter"], # type: Collection[FoundryHostedToolType]
|
||||
exclude_names=["internal_admin_tool"],
|
||||
)
|
||||
```
|
||||
|
||||
Helper signature:
|
||||
|
||||
```python
|
||||
type FoundryHostedToolType = Literal[
|
||||
"code_interpreter",
|
||||
"file_search",
|
||||
"image_generation",
|
||||
"mcp",
|
||||
"web_search",
|
||||
] | str
|
||||
|
||||
def select_toolbox_tools(
|
||||
tools: ToolboxVersionObject | Sequence[Tool | dict[str, Any]],
|
||||
*,
|
||||
include_names: Collection[str] | None = None,
|
||||
exclude_names: Collection[str] | None = None,
|
||||
include_types: Collection[FoundryHostedToolType] | None = None,
|
||||
exclude_types: Collection[FoundryHostedToolType] | None = None,
|
||||
predicate: Callable[[Tool | dict[str, Any]], bool] | None = None,
|
||||
) -> list[Tool | dict[str, Any]]:
|
||||
...
|
||||
```
|
||||
|
||||
Normalized name precedence for `include_names` / `exclude_names`:
|
||||
|
||||
1. MCP `server_label`
|
||||
2. generic tool `name`
|
||||
3. fallback tool `type`
|
||||
|
||||
This keeps `get_toolbox()` as a thin fetch API and makes selection an explicit,
|
||||
local post-processing step, while still allowing the ergonomic
|
||||
`select_toolbox_tools(toolbox, ...)` call shape.
|
||||
|
||||
## Native vs MCP consumption of a Foundry toolbox
|
||||
|
||||
A Foundry toolbox can be consumed two ways. This design adds new implementation work only for the first:
|
||||
|
||||
1. **Native consumption (in scope).** Tools execute inside Foundry's agent runtime. `get_toolbox()` returns the `ToolboxVersionObject` whose `.tools` attribute carries typed tool configs that the runtime interprets server-side. This design is specifically for `FoundryChatClient`-backed local agent composition.
|
||||
|
||||
2. **MCP consumption (already supported through existing MCP abstractions).** A Foundry toolbox can also be exposed as an MCP server. In that case, use the existing `MCPStreamableHTTPTool(name=..., url=...)` — it already handles this path with any chat client (Foundry, OpenAI, Anthropic, etc.). No new Foundry-specific API is needed for MCP-exposed toolboxes in this design.
|
||||
|
||||
### MCPStreamableHTTPTool example for a Foundry toolbox endpoint
|
||||
|
||||
If Foundry gives you an MCP endpoint for the toolbox (for example from the
|
||||
toolbox details UI / endpoint surface), the existing MCP client path is:
|
||||
|
||||
```python
|
||||
from agent_framework import Agent, MCPStreamableHTTPTool
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
toolbox_mcp = MCPStreamableHTTPTool(
|
||||
name="research_tools",
|
||||
url="https://<foundry-toolbox-mcp-endpoint>",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=OpenAIChatClient(),
|
||||
instructions="You are a research assistant.",
|
||||
tools=[toolbox_mcp],
|
||||
)
|
||||
```
|
||||
|
||||
This is a different integration shape than `get_toolbox(...).tools`:
|
||||
|
||||
- `get_toolbox(...).tools` = **native Foundry hosted-tool configs** interpreted by the
|
||||
Foundry runtime
|
||||
- `MCPStreamableHTTPTool(name=..., url=...)` = **live MCP server connection** to a
|
||||
toolbox endpoint
|
||||
|
||||
The design in this spec adds first-class support only for the native hosted-tool
|
||||
path. The MCP path is already served by the framework's existing MCP abstractions.
|
||||
|
||||
These paths are not unified because they have fundamentally different execution models. Native toolbox tools are declarative configs the Foundry runtime executes; MCP consumption is a live wire protocol to a running server.
|
||||
|
||||
**MCP authentication inside a toolbox** is handled server-side via `project_connection_id` on individual `MCPTool` entries (OAuth connection objects configured in the Foundry project). The client never holds bearer tokens. Consent flow handling (`CONSENT_REQUIRED` → user-visible consent URL) happens during `agent.run()`, not during toolbox fetching — see Non-goals.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Unit tests in `packages/foundry/tests/test_toolbox.py` with mocked `project_client.beta.toolboxes`. A single opt-in live round-trip, `test_integration_get_toolbox_round_trip_against_real_project`, is marked `@pytest.mark.integration`; it is skipped by default and only runs when the required Foundry credentials are available.
|
||||
|
||||
Coverage:
|
||||
|
||||
- `get_toolbox(name, version="v3")` — explicit version, single request. Assert `.get` not called, `.get_version` awaited once, returns `ToolboxVersionObject`.
|
||||
- `get_toolbox(name)` — default-version resolution. Assert `.get` then `.get_version` called in order with correct args.
|
||||
- Error propagation — `ResourceNotFoundError` from `.get` propagates unchanged.
|
||||
- Tool passthrough — heterogeneous tool list (`CodeInterpreterTool`, `MCPTool(project_connection_id=...)`) passes through unchanged. Asserts `project_connection_id` survives.
|
||||
- Agent integration smoke — `tools=toolbox` / `tools=[toolbox]` flatten to the underlying toolbox tools.
|
||||
- Multiple toolbox composition smoke — `tools=[toolbox_a, toolbox_b]` flattens into a single agent tool list.
|
||||
- `get_toolbox_tool_name()` — selection-name precedence is MCP `server_label`, then `name`, then `type`.
|
||||
- `select_toolbox_tools(toolbox, include_names=...)` — selects by normalized tool names directly from a fetched toolbox object.
|
||||
- `select_toolbox_tools(toolbox, include_types=...)` — selects by tool types with `Literal`-guided IDE completion.
|
||||
- `select_toolbox_tools(..., exclude_names=..., predicate=...)` — supports exclusion + custom predicates.
|
||||
|
||||
Deliberately **not** covered:
|
||||
- Runtime consent-flow handling for OAuth MCP tools (see Non-goals).
|
||||
- Toolbox discovery/listing (`list_toolboxes`, `list_toolbox_versions`) — deliberately left to the raw Azure SDK.
|
||||
- Full CRUD (`create_version`, `update`, `delete`) and server-side agent authoring — see Non-goals.
|
||||
|
||||
Live Foundry API integration is exercised only through the opt-in `@pytest.mark.integration` round-trip noted above; it is not part of the default test run.
|
||||
|
||||
## Framework dependency: `normalize_tools` flattening
|
||||
|
||||
The core `normalize_tools` function in `packages/core/agent_framework/_tools.py` already supports flattening composite tool inputs. Toolbox support extends that behavior so a fetched `ToolboxVersionObject` is treated as a composite tool source and flattened to its `.tools`.
|
||||
|
||||
That enables:
|
||||
|
||||
- `tools=toolbox`
|
||||
- `tools=[toolbox]`
|
||||
- `tools=[local_tool, toolbox]`
|
||||
- `tools=[toolbox_a, toolbox_b]`
|
||||
|
||||
while still keeping `select_toolbox_tools(toolbox.tools, ...)` available for partial selection before the final agent construction step.
|
||||
|
||||
## Telemetry
|
||||
|
||||
Telemetry for toolbox support has two separate goals:
|
||||
|
||||
1. **Observe toolbox API access** — `get_toolbox()`
|
||||
2. **Observe toolbox usage during agent runs** — when users pass toolbox-derived tools into `Agent(..., tools=...)`
|
||||
|
||||
### Request telemetry for toolbox API access
|
||||
|
||||
When Agent Framework constructs the `AIProjectClient` internally for `FoundryChatClient`, it already sets:
|
||||
|
||||
```python
|
||||
user_agent=AGENT_FRAMEWORK_USER_AGENT
|
||||
```
|
||||
|
||||
That means toolbox API requests made through:
|
||||
|
||||
- `project_client.beta.toolboxes.get(...)`
|
||||
- `project_client.beta.toolboxes.get_version(...)`
|
||||
|
||||
carry the standard MAF user-agent marker and can be queried in backend request logs the same way as other Foundry SDK calls made through framework-owned clients.
|
||||
|
||||
Important constraint: if the caller passes an already-constructed `project_client`, Agent Framework does **not** mutate it to inject the MAF user-agent. In that case, toolbox API request telemetry reflects whatever user-agent behavior that external client was configured with.
|
||||
|
||||
### Runtime telemetry for toolbox usage on agent runs
|
||||
|
||||
Tool-level telemetry already captures which hosted Foundry tools are available / invoked during agent execution. The remaining gap is **toolbox provenance**: once the user writes `tools=toolbox` (or otherwise flattens the toolbox into tool configs), the framework sees only raw tool configs and no longer knows which toolbox name/version supplied them.
|
||||
|
||||
The design for closing the **client-side** observability gap is **internal provenance tracking**, not user-supplied metadata and not a new public wrapper type.
|
||||
|
||||
#### Provenance model
|
||||
|
||||
Note: this section is still under investigation.
|
||||
|
||||
When `get_toolbox()` or `list_toolbox_versions()` returns a `ToolboxVersionObject`, Agent Framework will attach private provenance metadata to:
|
||||
|
||||
- the returned toolbox object
|
||||
- each tool inside `toolbox.tools`
|
||||
|
||||
Recommended shape (private, internal-only):
|
||||
|
||||
```python
|
||||
tool._maf_toolbox_sources = [
|
||||
{
|
||||
"id": toolbox.id,
|
||||
"name": toolbox.name,
|
||||
"version": toolbox.version,
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Key properties of this approach:
|
||||
|
||||
- **No new public API surface** — users still work with raw `ToolboxVersionObject` / `ToolboxObject`
|
||||
- **No user burden** — callers do not need to stamp metadata manually
|
||||
- **Provenance follows the tool objects** — works with:
|
||||
- `tools=toolbox.tools`
|
||||
- `tools=[toolbox_a.tools, toolbox_b.tools]`
|
||||
- `tools=[*toolbox_a.tools, *toolbox_b.tools]`
|
||||
- **Private attributes are not serialized** into the actual request payload sent to the model/service, so this metadata does not leak into the tool definition body
|
||||
|
||||
This is intentionally preferred over introducing a new public `FoundryToolbox` wrapper purely for telemetry, and preferred over a separate global provenance registry. The provenance lives on the existing tool objects so list-copying and chat-option merging naturally preserve it.
|
||||
|
||||
#### Span enrichment
|
||||
|
||||
When Agent / chat telemetry computes span attributes for a run, it should inspect the final tool list and aggregate the private toolbox provenance from any tool objects that carry it. The aggregated values are then emitted as attributes on the existing run/chat spans.
|
||||
|
||||
Suggested custom attributes:
|
||||
|
||||
- `agent_framework.foundry.toolbox.ids`
|
||||
- `agent_framework.foundry.toolbox.names`
|
||||
- `agent_framework.foundry.toolbox.versions`
|
||||
- or a single compact attribute such as `agent_framework.foundry.toolbox.sources=["research_tools@1","some_other_tools@3"]`
|
||||
|
||||
The single compact `toolbox.sources` form is preferred for initial implementation because it is easy to query and easy to render from combined tool lists.
|
||||
|
||||
#### Scope of telemetry changes
|
||||
|
||||
This design does **not** require new spans. It enriches existing telemetry:
|
||||
|
||||
- toolbox API access continues to rely on request logs + Azure SDK distributed tracing + MAF user-agent
|
||||
- agent/chat execution spans gain toolbox provenance attributes when toolbox-derived tools are present
|
||||
|
||||
Implementation-wise, this design most likely touches:
|
||||
|
||||
- `packages/foundry/agent_framework_foundry/_tools.py` — to stamp provenance on fetched toolbox objects / tools
|
||||
- `packages/core/agent_framework/observability.py` — to aggregate provenance into span attributes
|
||||
|
||||
#### Important limitation: no server-side toolbox telemetry solution yet
|
||||
|
||||
Private provenance attached to tool objects is only useful on the client side. It
|
||||
does **not** go over the wire to the Foundry service because those private fields
|
||||
are intentionally not serialized into the request payload.
|
||||
|
||||
That means this design can support:
|
||||
|
||||
- local OpenTelemetry / exporter spans emitted by Agent Framework
|
||||
- local attribution of a run to one or more fetched toolboxes
|
||||
|
||||
but it does **not** solve:
|
||||
|
||||
- server-side request-log attribution of a model/tool run back to a toolbox
|
||||
- backend/database queries that need the service itself to know "this tool came from toolbox X"
|
||||
|
||||
At the moment, we do not have a satisfactory design for server-side toolbox
|
||||
telemetry. The service would require additional structured information on the
|
||||
request, and there is no accepted mechanism in this design yet for projecting
|
||||
toolbox provenance into a server-visible field/header/metadata shape.
|
||||
|
||||
So the telemetry story in this spec is explicitly limited to **client-side
|
||||
toolbox telemetry**. Server-side toolbox attribution remains an open question and
|
||||
requires either:
|
||||
|
||||
- new service/API support, or
|
||||
- a later framework design for emitting additional server-visible request metadata.
|
||||
|
||||
#### Deliberate non-goals for telemetry
|
||||
|
||||
- No requirement for users to pass explicit toolbox metadata in `default_options["metadata"]` or `run(..., options=...)`
|
||||
- No new public `FoundryToolbox` wrapper type just to preserve attribution
|
||||
- No attempted server-side attribution mechanism in this design (for example a custom request header or request metadata field) until there is a validated end-to-end contract for it
|
||||
|
||||
## Non-goals / Future Work
|
||||
|
||||
Explicitly out of scope for this design. Each is a separate design and PR when needed.
|
||||
|
||||
1. **Create/update/delete toolboxes from code.** CRUD is rare in agent consumption flows. Users who need it drop to `client.project_client.beta.toolboxes.create_version(...)`, `.update(...)`, `.delete(...)` directly.
|
||||
|
||||
2. **Server-side agent authoring from toolbox.** Creating a `PromptAgentDefinition(tools=toolbox.tools)` + `client.agents.create_version(...)` is a future feature covering agent authoring from code. The toolbox read API provides the building blocks; the authoring helpers are a separate design.
|
||||
|
||||
3. **OAuth consent-flow runtime handling.** When a toolbox contains MCP tools with `project_connection_id` pointing to an OAuth connection, the runtime may return `CONSENT_REQUIRED` mid-run. This is a runtime concern separate from toolbox fetching.
|
||||
|
||||
4. **Live integration tests.** This PR ships unit tests only.
|
||||
|
||||
5. **Toolbox caching or refresh APIs.** Each `get_toolbox()` call hits the network. Users who want caching wrap the call themselves.
|
||||
@@ -1,625 +0,0 @@
|
||||
# CodeAct .NET implementation
|
||||
|
||||
This document describes the .NET realization of the CodeAct design in
|
||||
[`docs/decisions/0024-codeact-integration.md`](../../decisions/0024-codeact-integration.md).
|
||||
|
||||
This document is intentionally focused on the .NET design and public API surface.
|
||||
The initial public .NET type described here is `HyperlightCodeActProvider`. Future .NET backends, such as Monty, should follow the same conceptual model with their own concrete provider types rather than through a public abstract base class or a public executor parameter.
|
||||
|
||||
## What is the goal of this feature?
|
||||
|
||||
Goals:
|
||||
- .NET developers can enable CodeAct through an `AIContextProvider`-based integration.
|
||||
- Developers can configure a provider-owned CodeAct tool set that is separate from the agent's direct tool surface.
|
||||
- Developers can use the same `execute_code` concept for both tool-enabled CodeAct and a standard code interpreter tool implementation.
|
||||
- Developers can swap execution backends over time, starting with Hyperlight while keeping room for alternatives.
|
||||
- Developers can configure execution capabilities such as workspace mounts and outbound network allow lists in a portable way.
|
||||
|
||||
Success Metric:
|
||||
- .NET samples exist for both a tool-enabled CodeAct mode and a standard interpreter mode.
|
||||
|
||||
Implementation-free outcome:
|
||||
- A .NET developer can attach a backend-specific CodeAct provider, choose which tools are available inside CodeAct, and configure execution capabilities without rewriting the function invocation loop or ChatClient pipeline.
|
||||
|
||||
## What is the problem being solved?
|
||||
|
||||
The cross-SDK problem statement and decision rationale live in the [ADR](../../decisions/0024-codeact-integration.md). The items below narrow that statement to .NET-specific design concerns:
|
||||
|
||||
- Today, the easiest way to prototype CodeAct in .NET is to manually configure an `AIFunction` and wire instructions — this is fragile and requires understanding internal sandbox lifecycle details.
|
||||
- There is no first-class .NET design that simultaneously covers Hyperlight-backed CodeAct now, future backend-specific providers, and both tool-enabled and interpreter modes.
|
||||
- Sandbox capabilities such as mounted file access and outbound network access need a portable configuration model instead of ad hoc backend-specific wiring.
|
||||
- Approval behavior needs to be explicit and configurable, mapping to .NET's existing `ApprovalRequiredAIFunction` wrapper mechanism.
|
||||
|
||||
## API Changes
|
||||
|
||||
### CodeAct contract
|
||||
|
||||
#### Terminology
|
||||
|
||||
- **CodeAct** is the primary term.
|
||||
- `execute_code` is the model-facing tool name used by the initial .NET provider in this spec.
|
||||
- Tool-enabled versus interpreter behavior is derived from the presence of CodeAct-managed tools, not from a separate public profile object.
|
||||
|
||||
#### Provider-owned CodeAct tool registry
|
||||
|
||||
A concrete .NET CodeAct provider owns the set of tools available through `call_tool(...)` inside CodeAct.
|
||||
|
||||
Rules:
|
||||
- Only tools explicitly configured on the concrete provider instance are available inside CodeAct.
|
||||
- The provider must not infer its CodeAct-managed tool set from the agent's direct tool configuration (`ChatClientAgentOptions.Tools` or `AIContext.Tools`).
|
||||
- Exclusive versus mixed behavior is achieved by where tools are configured, not by rewriting the agent's direct tool list.
|
||||
|
||||
Implications:
|
||||
- **CodeAct-only tool**: configured on the concrete CodeAct provider only.
|
||||
- **Direct-only tool**: configured on the agent only.
|
||||
- **Tool available both ways**: configured on both the agent and the concrete CodeAct provider.
|
||||
|
||||
#### Managing tools and capabilities after provider construction
|
||||
|
||||
There is no separate runtime setup object in the .NET design. CodeAct tools, file mounts, and outbound network allow-list state are managed directly on the provider through CRUD-style registry methods.
|
||||
|
||||
Preferred pattern:
|
||||
- `AddTools(params AIFunction[] tools) -> void`
|
||||
- `GetTools() -> IReadOnlyList<AIFunction>`
|
||||
- `RemoveTools(params string[] names) -> void`
|
||||
- `ClearTools() -> void`
|
||||
- `AddFileMounts(params FileMount[] mounts) -> void`
|
||||
- `GetFileMounts() -> IReadOnlyList<FileMount>`
|
||||
- `RemoveFileMounts(params string[] mountPaths) -> void`
|
||||
- `ClearFileMounts() -> void`
|
||||
- `AddAllowedDomains(params AllowedDomain[] domains) -> void`
|
||||
- `GetAllowedDomains() -> IReadOnlyList<AllowedDomain>`
|
||||
- `RemoveAllowedDomains(params string[] targets) -> void`
|
||||
- `ClearAllowedDomains() -> void`
|
||||
|
||||
Requirements:
|
||||
- The provider-owned CodeAct tool registry is keyed by tool name (from `AIFunction.Name`).
|
||||
- `AddTools(...)` adds new tools and replaces an existing provider-owned registration when the same tool name is added again.
|
||||
- `GetTools()` returns the provider's current configured CodeAct tool registry.
|
||||
- `RemoveTools(...)` removes provider-owned CodeAct tools by name.
|
||||
- `ClearTools()` removes all provider-owned CodeAct tools.
|
||||
- File mounts are keyed by sandbox mount path.
|
||||
- `AddFileMounts(...)` adds new file mounts and replaces an existing mount when the same mount path is added again.
|
||||
- `GetFileMounts()` returns the provider's current configured file mounts.
|
||||
- `RemoveFileMounts(...)` removes file mounts by mount path.
|
||||
- `ClearFileMounts()` removes all configured file mounts.
|
||||
- Allowed domains are keyed by normalized target string.
|
||||
- `AddAllowedDomains(...)` adds allow-list entries and replaces an existing entry when the same target is added again.
|
||||
- `GetAllowedDomains()` returns the current outbound allow-list entries.
|
||||
- `RemoveAllowedDomains(...)` removes allow-list entries by target.
|
||||
- `ClearAllowedDomains()` removes all configured allow-list entries.
|
||||
- Tool, file-mount, and network-allow-list mutations affect subsequent runs only; runs already in progress keep the snapshot captured at run start.
|
||||
- The provider must snapshot its effective tool registry and capability state at the start of each run so concurrent execution remains deterministic.
|
||||
|
||||
#### Approval model
|
||||
|
||||
The initial .NET design follows the ADR's bundled approval decision and maps to the existing `ApprovalRequiredAIFunction` wrapper from `Microsoft.Extensions.AI.Abstractions`:
|
||||
|
||||
- The provider exposes a default `ApprovalMode` for `execute_code` (enum: `CodeActApprovalMode.AlwaysRequire` / `CodeActApprovalMode.NeverRequire`).
|
||||
|
||||
Effective `execute_code` approval is computed as follows:
|
||||
|
||||
- If the provider default is `AlwaysRequire`, `execute_code` requires approval.
|
||||
- If the provider default is `NeverRequire`, the provider evaluates the provider-owned CodeAct tool registry snapshot for that run.
|
||||
- If every provider-owned CodeAct tool in that snapshot is not an `ApprovalRequiredAIFunction`, `execute_code` does not require approval.
|
||||
- If any provider-owned CodeAct tool in that snapshot is an `ApprovalRequiredAIFunction`, `execute_code` requires approval, even if the generated code may not call that tool.
|
||||
- When the effective approval resolves to `AlwaysRequire`, the generated `execute_code` function is wrapped in `ApprovalRequiredAIFunction` before being added to the `AIContext.Tools`.
|
||||
- Provider-owned tool calls made through `call_tool(...)` during that execution run use the approval already determined for `execute_code`.
|
||||
- Direct-only agent tools are excluded from this calculation.
|
||||
- File and network capabilities do not create a separate runtime approval check in the initial model; configuring them on the provider is itself the approval for those capabilities.
|
||||
|
||||
This is intentionally conservative and matches the shape of the existing .NET function-tool approval flow, where `ApprovalRequiredAIFunction` signals to the `ChatClientAgent` that user approval is needed before invocation.
|
||||
|
||||
#### Shared execution flow
|
||||
|
||||
On each run:
|
||||
1. `ProvideAIContextAsync(...)` snapshots the current CodeAct-managed tool registry and capability settings.
|
||||
2. Computes the effective approval requirement for `execute_code` from the provider default plus the snapshotted tool registry.
|
||||
3. Builds provider-defined instructions.
|
||||
4. Builds a run-scoped `execute_code` `AIFunction` from the snapshot (optionally wrapped in `ApprovalRequiredAIFunction`).
|
||||
5. Returns an `AIContext` containing the instructions and `execute_code` tool.
|
||||
6. When `execute_code` is invoked by the model, the run-scoped function creates or reuses an execution environment.
|
||||
7. If the current provider mode exposes host tools, `call_tool(...)` is bound only to the provider-owned tool registry snapshot.
|
||||
8. Code is executed and results converted to a JSON result string.
|
||||
|
||||
Caching rules:
|
||||
- The Hyperlight backend supports snapshots: the provider caches a reusable clean snapshot after the first sandbox initialization.
|
||||
- No mutable per-run execution state may be shared across concurrent runs.
|
||||
- In-memory interpreter state does not persist across separate `execute_code` calls.
|
||||
- Configured workspace files, mounted files, and any writable artifact/output area are the supported persistence mechanism across calls when the backend exposes them.
|
||||
|
||||
### .NET public API
|
||||
|
||||
#### Core types
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Represents a host-to-sandbox file mount configuration.
|
||||
/// </summary>
|
||||
/// <param name="HostPath">Absolute or relative path on the host filesystem.</param>
|
||||
/// <param name="MountPath">Path inside the sandbox (e.g. "/input/data.csv").</param>
|
||||
public sealed record FileMount(string HostPath, string MountPath);
|
||||
|
||||
/// <summary>
|
||||
/// Represents an outbound network allow-list entry.
|
||||
/// </summary>
|
||||
/// <param name="Target">URL or domain (e.g. "https://api.github.com").</param>
|
||||
/// <param name="Methods">
|
||||
/// Optional HTTP methods to allow (e.g. ["GET", "POST"]).
|
||||
/// Null allows all methods supported by the backend.
|
||||
/// </param>
|
||||
public sealed record AllowedDomain(string Target, IReadOnlyList<string>? Methods = null);
|
||||
|
||||
/// <summary>
|
||||
/// Controls the approval behavior for execute_code invocations.
|
||||
/// </summary>
|
||||
public enum CodeActApprovalMode
|
||||
{
|
||||
/// <summary>execute_code always requires user approval.</summary>
|
||||
AlwaysRequire,
|
||||
|
||||
/// <summary>
|
||||
/// Approval is derived from the provider-owned tool registry:
|
||||
/// if any tool is an ApprovalRequiredAIFunction, execute_code requires approval.
|
||||
/// </summary>
|
||||
NeverRequire,
|
||||
}
|
||||
```
|
||||
|
||||
#### HyperlightCodeActProvider
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// An AIContextProvider that enables CodeAct execution through the
|
||||
/// Hyperlight sandbox backend.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This provider injects an <c>execute_code</c> tool into the model-facing
|
||||
/// tool surface and builds CodeAct guidance instructions. Guest code executed
|
||||
/// through <c>execute_code</c> runs in an isolated Hyperlight sandbox with
|
||||
/// snapshot/restore for clean state per invocation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If no CodeAct-managed tools are configured, the provider uses
|
||||
/// interpreter-style behavior. If one or more CodeAct-managed tools are
|
||||
/// configured, the provider uses tool-enabled behavior and exposes
|
||||
/// <c>call_tool(...)</c> inside the sandbox bound to the configured tools.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class HyperlightCodeActProvider : AIContextProvider, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new HyperlightCodeActProvider.
|
||||
/// </summary>
|
||||
/// <param name="options">Configuration options for the provider.</param>
|
||||
public HyperlightCodeActProvider(HyperlightCodeActProviderOptions options);
|
||||
|
||||
// ----- Tool registry -----
|
||||
|
||||
/// <summary>Adds tools to the provider-owned CodeAct tool registry.</summary>
|
||||
public void AddTools(params AIFunction[] tools);
|
||||
|
||||
/// <summary>Returns the current CodeAct-managed tools.</summary>
|
||||
public IReadOnlyList<AIFunction> GetTools();
|
||||
|
||||
/// <summary>Removes tools by name from the CodeAct tool registry.</summary>
|
||||
public void RemoveTools(params string[] names);
|
||||
|
||||
/// <summary>Removes all CodeAct-managed tools.</summary>
|
||||
public void ClearTools();
|
||||
|
||||
// ----- File mounts -----
|
||||
|
||||
/// <summary>Adds file mount configurations.</summary>
|
||||
public void AddFileMounts(params FileMount[] mounts);
|
||||
|
||||
/// <summary>Returns the current file mount configurations.</summary>
|
||||
public IReadOnlyList<FileMount> GetFileMounts();
|
||||
|
||||
/// <summary>Removes file mounts by sandbox mount path.</summary>
|
||||
public void RemoveFileMounts(params string[] mountPaths);
|
||||
|
||||
/// <summary>Removes all file mount configurations.</summary>
|
||||
public void ClearFileMounts();
|
||||
|
||||
// ----- Network allow-list -----
|
||||
|
||||
/// <summary>Adds outbound network allow-list entries.</summary>
|
||||
public void AddAllowedDomains(params AllowedDomain[] domains);
|
||||
|
||||
/// <summary>Returns the current outbound allow-list entries.</summary>
|
||||
public IReadOnlyList<AllowedDomain> GetAllowedDomains();
|
||||
|
||||
/// <summary>Removes allow-list entries by target.</summary>
|
||||
public void RemoveAllowedDomains(params string[] targets);
|
||||
|
||||
/// <summary>Removes all outbound allow-list entries.</summary>
|
||||
public void ClearAllowedDomains();
|
||||
|
||||
// ----- Lifecycle -----
|
||||
|
||||
/// <summary>Releases the sandbox and all associated native resources.</summary>
|
||||
public void Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
#### HyperlightCodeActProviderOptions
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Configuration options for <see cref="HyperlightCodeActProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class HyperlightCodeActProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The sandbox backend to use. Default is <c>Wasm</c>.
|
||||
/// </summary>
|
||||
public SandboxBackend Backend { get; set; } = SandboxBackend.Wasm;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the guest module (.wasm or .aot file).
|
||||
/// Required for the Wasm backend; not needed for JavaScript.
|
||||
/// When null, the provider attempts to locate the default packaged
|
||||
/// Python guest module.
|
||||
/// </summary>
|
||||
public string? ModulePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Guest heap size. Accepts human-readable strings ("50Mi", "2Gi")
|
||||
/// or raw byte values. Null uses the backend default.
|
||||
/// </summary>
|
||||
public string? HeapSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Guest stack size. Accepts human-readable strings ("35Mi")
|
||||
/// or raw byte values. Null uses the backend default.
|
||||
/// </summary>
|
||||
public string? StackSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial set of CodeAct-managed tools available inside the sandbox.
|
||||
/// </summary>
|
||||
public IEnumerable<AIFunction>? Tools { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default approval mode for the execute_code tool.
|
||||
/// Default is <see cref="CodeActApprovalMode.NeverRequire"/>.
|
||||
/// </summary>
|
||||
public CodeActApprovalMode ApprovalMode { get; set; } = CodeActApprovalMode.NeverRequire;
|
||||
|
||||
/// <summary>
|
||||
/// Optional workspace root directory on the host.
|
||||
/// When set, it is exposed as the sandbox's input directory.
|
||||
/// </summary>
|
||||
public string? WorkspaceRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial file mount configurations.
|
||||
/// </summary>
|
||||
public IEnumerable<FileMount>? FileMounts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial outbound network allow-list entries.
|
||||
/// </summary>
|
||||
public IEnumerable<AllowedDomain>? AllowedDomains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// State key used to store provider state in AgentSession.StateBag.
|
||||
/// Defaults to "HyperlightCodeActProvider". Override when using
|
||||
/// multiple provider instances on the same agent.
|
||||
/// </summary>
|
||||
public string? StateKey { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### Provider implementation contract
|
||||
|
||||
The concrete provider plugs into the existing .NET `AIContextProvider` surface from `Microsoft.Agents.AI.Abstractions`.
|
||||
|
||||
Required override:
|
||||
- `ProvideAIContextAsync(InvokingContext, CancellationToken) -> ValueTask<AIContext>`
|
||||
|
||||
`ProvideAIContextAsync(...)` is responsible for:
|
||||
- snapshotting the current CodeAct-managed tool registry and capability settings for the run,
|
||||
- computing the effective approval requirement for `execute_code` from the provider default and the snapshotted tool registry,
|
||||
- building a short CodeAct guidance instruction string,
|
||||
- building a run-scoped `execute_code` `AIFunction` from the snapshot,
|
||||
- optionally wrapping it in `ApprovalRequiredAIFunction` when approval is required,
|
||||
- and returning an `AIContext` with `Instructions` and `Tools` set.
|
||||
|
||||
These steps run on every invocation rather than once at construction time because the provider supports CRUD mutations between runs, concurrent runs need independent snapshots, and the effective approval and instructions depend on the tool registry state captured at run start.
|
||||
|
||||
The provider overrides `StateKeys` to return the configured `StateKey` from options, enabling multiple provider instances on the same agent without key collisions.
|
||||
|
||||
Mutating the provider after `ProvideAIContextAsync(...)` has captured a run-scoped snapshot is allowed, but it affects subsequent runs only. Provider implementations synchronize state capture and CRUD operations so shared provider instances remain safe across concurrent runs.
|
||||
|
||||
#### AIFunction-to-sandbox tool bridging
|
||||
|
||||
The Hyperlight sandbox's `RegisterTool(name, Func<string, string>)` accepts a synchronous JSON-in / JSON-out delegate. Provider-owned CodeAct tools are `AIFunction` instances that are async and cancellation-aware.
|
||||
|
||||
Bridging strategy:
|
||||
- At sandbox initialization time, the provider registers each CodeAct-managed tool with the sandbox using the raw JSON overload: `RegisterTool(name, Func<string, string>)`.
|
||||
- When the sandbox guest calls `call_tool("name", ...)`, the bridge delegate:
|
||||
1. Deserializes the JSON arguments.
|
||||
2. Invokes `AIFunction.InvokeAsync(...)` synchronously (via `GetAwaiter().GetResult()`) since the sandbox FFI callback is inherently synchronous.
|
||||
3. Serializes the result back to JSON.
|
||||
- This sync-over-async bridge is a known pragmatic trade-off constrained by the Hyperlight FFI boundary. It is safe because:
|
||||
- Sandbox execution already runs on the thread pool (via `Task.Run`).
|
||||
- The FFI callback runs on a worker thread with no synchronization context.
|
||||
- If the Hyperlight .NET SDK later adds async tool registration, the bridge should migrate to that.
|
||||
|
||||
#### Runtime behavior
|
||||
|
||||
- `ProvideAIContextAsync(...)` adds a short CodeAct guidance block through `AIContext.Instructions`.
|
||||
- `ProvideAIContextAsync(...)` adds `execute_code` through `AIContext.Tools`.
|
||||
- The detailed `call_tool(...)`, sandbox-tool, and capability guidance is carried by the `execute_code` function's `Description`.
|
||||
- `execute_code` invokes the configured Hyperlight sandbox guest.
|
||||
- If the current CodeAct tool registry snapshot is non-empty, the runtime injects `call_tool(...)` bound to the provider-owned tool registry.
|
||||
- The provider does not inspect or mutate the agent's `ChatClientAgentOptions.Tools` or the incoming `AIContext.Tools` to determine its CodeAct tool set.
|
||||
- The provider snapshots the current CodeAct tool registry and capability state at run start, so later registry and allow-list mutations only affect future runs.
|
||||
- Interpreter versus tool-enabled behavior is derived from the presence of CodeAct-managed tools.
|
||||
- `execute_code` is traced like a normal tool invocation within the surrounding agent run.
|
||||
|
||||
#### Backend integration
|
||||
|
||||
Initial public provider:
|
||||
- `HyperlightCodeActProvider`
|
||||
|
||||
Backend-specific notes:
|
||||
- **Hyperlight**
|
||||
- The provider internally creates a `SandboxBuilder` from the options and uses the `Sandbox` API from `HyperlightSandbox.Api`.
|
||||
- The provider uses snapshot/restore to ensure clean execution state per `execute_code` invocation: a "warm" snapshot is taken after the first no-op initialization run, and restored before each subsequent execution.
|
||||
- File access maps to Hyperlight Sandbox's `WithInputDir()` / `WithOutputDir()` / `WithTempOutput()` capability model.
|
||||
- Network access is denied by default and is enabled through `Sandbox.AllowDomain(...)` per-target allow-list entries.
|
||||
- Guest module resolution: if `ModulePath` is null for the Wasm backend, the provider attempts to locate a packaged Python guest module (equivalent to the Python SDK's `python_guest.path` resolution).
|
||||
|
||||
#### Capability handling
|
||||
|
||||
Capabilities are first-class `HyperlightCodeActProviderOptions` properties and provider-managed CRUD surfaces:
|
||||
- `WorkspaceRoot`
|
||||
- `FileMounts`
|
||||
- `AllowedDomains`
|
||||
|
||||
Enabling access means:
|
||||
- Configuring `WorkspaceRoot` or any `FileMounts` enables the sandbox filesystem surface exposed through `/input` and `/output`.
|
||||
- Leaving both `WorkspaceRoot` and `FileMounts` unset means no filesystem surface is configured.
|
||||
- Adding any `AllowedDomains` entry enables outbound access only for the configured targets; leaving it empty means network access is disabled without a separate network mode flag.
|
||||
|
||||
Backends may implement stricter semantics than these top-level settings.
|
||||
|
||||
#### Execution output representation
|
||||
|
||||
Backend execution output maps to a JSON result string returned from the `execute_code` `AIFunction`:
|
||||
|
||||
```json
|
||||
{
|
||||
"stdout": "Hello world\n",
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
Execution failures should surface readable error text in the `stderr` field and a non-zero `exit_code`. Timeouts, out-of-memory conditions, backend crashes, and similar sandbox failures are all `execute_code` failures and should surface as structured error results. Partial textual or file outputs may be returned only when the backend can report them unambiguously.
|
||||
|
||||
#### `execute_code` input contract
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Code to execute using the provider's configured backend/runtime behavior."
|
||||
}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Thread safety and concurrency
|
||||
|
||||
- All CRUD methods (`AddTools`, `RemoveTools`, `AddFileMounts`, etc.) are synchronized via an internal lock.
|
||||
- `ProvideAIContextAsync(...)` acquires the lock to snapshot current state, then releases it before building the run-scoped function. The run-scoped function closes over the immutable snapshot, not mutable provider state.
|
||||
- Concurrent `execute_code` invocations from different runs use independent sandbox instances or synchronized access to a shared sandbox with snapshot/restore.
|
||||
- Workspace directories (`WorkspaceRoot`, `FileMounts`) are external shared state: concurrent runs against the same workspace can race on files. This is the user's responsibility to manage (e.g., by using per-run output directories or separate provider instances).
|
||||
|
||||
### HyperlightExecuteCodeFunction
|
||||
|
||||
The provider package also exports a standalone `HyperlightExecuteCodeFunction` for direct-tool scenarios where a provider lifecycle is not needed. This is the .NET equivalent of the Python `HyperlightExecuteCodeTool`.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A standalone execute_code AIFunction backed by a Hyperlight sandbox.
|
||||
/// Use this for manual/static wiring when the AIContextProvider lifecycle
|
||||
/// is not needed.
|
||||
/// </summary>
|
||||
public sealed class HyperlightExecuteCodeFunction : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new standalone code execution function.
|
||||
/// </summary>
|
||||
/// <param name="options">Configuration options.</param>
|
||||
public HyperlightExecuteCodeFunction(HyperlightCodeActProviderOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Returns this as an AIFunction for direct registration on an agent.
|
||||
/// When approval is required, the returned function is wrapped in
|
||||
/// ApprovalRequiredAIFunction.
|
||||
/// </summary>
|
||||
public AIFunction AsAIFunction();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a CodeAct instruction string describing the available
|
||||
/// tools and capabilities.
|
||||
/// </summary>
|
||||
/// <param name="toolsVisibleToModel">
|
||||
/// When false, the instructions include full tool descriptions
|
||||
/// (for use when tools are only accessible through CodeAct).
|
||||
/// When true, instructions are abbreviated (tools are already
|
||||
/// visible to the model as direct tools).
|
||||
/// </param>
|
||||
public string BuildInstructions(bool toolsVisibleToModel = false);
|
||||
|
||||
/// <summary>Releases sandbox resources.</summary>
|
||||
public void Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### Internal implementation structure
|
||||
|
||||
The provider and standalone function share internal helpers:
|
||||
|
||||
```
|
||||
Microsoft.Agents.AI.Hyperlight/
|
||||
├── HyperlightCodeActProvider.cs // AIContextProvider implementation
|
||||
├── HyperlightCodeActProviderOptions.cs // Options record
|
||||
├── HyperlightExecuteCodeFunction.cs // Standalone AIFunction for manual wiring
|
||||
├── FileMount.cs // File mount record
|
||||
├── AllowedDomain.cs // Network allow-list record
|
||||
├── CodeActApprovalMode.cs // Approval enum
|
||||
├── Internal/
|
||||
│ ├── SandboxExecutor.cs // Manages sandbox lifecycle, snapshot/restore
|
||||
│ ├── InstructionBuilder.cs // Builds CodeAct instruction strings
|
||||
│ └── ToolBridge.cs // AIFunction ↔ Sandbox.RegisterTool adapter
|
||||
```
|
||||
|
||||
`SandboxExecutor` encapsulates:
|
||||
- Creating and configuring a `Sandbox` from options.
|
||||
- Performing the initial no-op warm-up and snapshot.
|
||||
- Registering bridged tools via `ToolBridge`.
|
||||
- Restoring to the clean snapshot before each execution.
|
||||
- Translating `ExecutionResult` to a JSON string.
|
||||
|
||||
`InstructionBuilder` generates:
|
||||
- A short CodeAct guidance block for `AIContext.Instructions`.
|
||||
- A detailed `execute_code` description including `call_tool(...)` signatures and capability documentation.
|
||||
|
||||
`ToolBridge` handles:
|
||||
- Reflecting `AIFunction` metadata to build the sandbox tool registration.
|
||||
- The sync-over-async invocation bridge.
|
||||
|
||||
## E2E Code Samples
|
||||
|
||||
### Tool-enabled CodeAct mode
|
||||
|
||||
```csharp
|
||||
var fetchDocs = AIFunctionFactory.Create(FetchDocs, name: "fetch_docs");
|
||||
var queryData = AIFunctionFactory.Create(QueryData, name: "query_data");
|
||||
var lookupUser = AIFunctionFactory.Create(LookupUser, name: "lookup_user");
|
||||
|
||||
var codeact = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions
|
||||
{
|
||||
Tools = [fetchDocs, queryData],
|
||||
WorkspaceRoot = "./workdir",
|
||||
AllowedDomains = [new AllowedDomain("api.github.com", ["GET"])],
|
||||
});
|
||||
codeact.AddTools(lookupUser);
|
||||
|
||||
var sendEmail = AIFunctionFactory.Create(SendEmail, name: "send_email");
|
||||
|
||||
var agent = chatClient.AsAIAgent(
|
||||
instructions: "You are a helpful assistant.",
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
Tools = [sendEmail], // direct-only tool
|
||||
AIContextProviders = [codeact],
|
||||
});
|
||||
|
||||
await using var session = await agent.CreateSessionAsync();
|
||||
var response = await agent.InvokeAsync("Analyze the latest docs", session);
|
||||
```
|
||||
|
||||
### Standard code interpreter mode
|
||||
|
||||
```csharp
|
||||
var codeact = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions
|
||||
{
|
||||
WorkspaceRoot = "./data",
|
||||
});
|
||||
|
||||
var agent = chatClient.AsAIAgent(
|
||||
instructions: "You are a code interpreter.",
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [codeact],
|
||||
});
|
||||
```
|
||||
|
||||
### Manual static wiring (no provider lifecycle)
|
||||
|
||||
When the tool registry and capability configuration are fixed, the provider lifecycle can be skipped entirely. Build the `execute_code` function and instructions once and pass them directly to the agent:
|
||||
|
||||
```csharp
|
||||
using var executeCode = new HyperlightExecuteCodeFunction(
|
||||
new HyperlightCodeActProviderOptions
|
||||
{
|
||||
Tools = [fetchDocs, queryData],
|
||||
WorkspaceRoot = "./workdir",
|
||||
AllowedDomains = [new AllowedDomain("api.github.com", ["GET"])],
|
||||
});
|
||||
|
||||
var codeactInstructions = executeCode.BuildInstructions(toolsVisibleToModel: false);
|
||||
|
||||
var agent = chatClient.AsAIAgent(
|
||||
instructions: $"You are a helpful assistant.\n\n{codeactInstructions}",
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
Tools = [sendEmail, executeCode.AsAIFunction()],
|
||||
});
|
||||
```
|
||||
|
||||
### With approval required
|
||||
|
||||
```csharp
|
||||
var sensitiveAction = new ApprovalRequiredAIFunction(
|
||||
AIFunctionFactory.Create(DeleteRecords, name: "delete_records"));
|
||||
|
||||
var codeact = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions
|
||||
{
|
||||
Tools = [fetchDocs, sensitiveAction], // sensitiveAction triggers approval
|
||||
});
|
||||
|
||||
// execute_code will be wrapped in ApprovalRequiredAIFunction because
|
||||
// at least one managed tool (delete_records) requires approval.
|
||||
var agent = chatClient.AsAIAgent(
|
||||
instructions: "You are a helpful assistant.",
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [codeact],
|
||||
});
|
||||
```
|
||||
|
||||
## Relationship to hyperlight-sandbox .NET SDK
|
||||
|
||||
This design depends on the .NET SDK being added in [hyperlight-dev/hyperlight-sandbox#46](https://github.com/hyperlight-dev/hyperlight-sandbox/pull/46). Key types consumed from that SDK:
|
||||
|
||||
| hyperlight-sandbox type | Used for |
|
||||
|---|---|
|
||||
| `Sandbox` | Core sandbox lifecycle: `Run()`, `RegisterTool()`, `AllowDomain()`, `Snapshot()`, `Restore()` |
|
||||
| `SandboxBuilder` | Fluent sandbox construction from provider options |
|
||||
| `SandboxBackend` | Backend selection (Wasm, JavaScript) |
|
||||
| `ExecutionResult` | Capturing stdout, stderr, exit code from guest execution |
|
||||
| `SandboxSnapshot` | Checkpoint/restore for clean state per execution |
|
||||
|
||||
The provider package (`Microsoft.Agents.AI.Hyperlight`) takes a NuGet dependency on `Hyperlight.HyperlightSandbox.Api` and `Microsoft.Extensions.AI.Abstractions`. It does **not** depend on `HyperlightSandbox.Extensions.AI` (`CodeExecutionTool`) — the provider implements its own sandbox lifecycle management with run-scoped snapshots to support concurrent invocations safely.
|
||||
|
||||
## Package structure
|
||||
|
||||
The CodeAct Hyperlight provider ships as an optional NuGet package:
|
||||
- **Package**: `Microsoft.Agents.AI.Hyperlight`
|
||||
- **Dependencies**:
|
||||
- `Microsoft.Agents.AI.Abstractions` (for `AIContextProvider`, `AIContext`)
|
||||
- `Microsoft.Extensions.AI.Abstractions` (for `AIFunction`, `ApprovalRequiredAIFunction`)
|
||||
- `Hyperlight.HyperlightSandbox.Api` (for sandbox API)
|
||||
- **Target framework**: `net8.0`
|
||||
|
||||
This keeps CodeAct and its native sandbox dependencies optional — users who do not need CodeAct do not take on the Hyperlight installation and dependency footprint.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Guest module distribution**: How should the default Python guest module (`.aot` file) be distributed for .NET consumers? Options include a separate NuGet package with native assets, a runtime download, or requiring users to build/provide their own.
|
||||
2. **Async tool registration**: If the Hyperlight .NET SDK adds async tool callback support in a future release, the sync-over-async bridge should be replaced. This is tracked as a known technical debt item.
|
||||
3. **Output file access**: The Hyperlight sandbox exposes `GetOutputFiles()` and `OutputPath` for retrieving files written by guest code. The initial design returns these as part of the JSON result. A future iteration could surface output files as framework-native content (e.g., `DataContent` or URI references).
|
||||
4. **Multiple sandbox instances for concurrency**: The current design uses synchronized access to a single sandbox with snapshot/restore. An alternative pooling strategy (one sandbox per concurrent run) could improve throughput at the cost of memory. This is deferred to implementation time.
|
||||
@@ -1,385 +0,0 @@
|
||||
# CodeAct Python implementation
|
||||
|
||||
This document describes the Python realization of the CodeAct design in
|
||||
[`docs/decisions/0024-codeact-integration.md`](../../decisions/0024-codeact-integration.md).
|
||||
|
||||
This document is intentionally focused on the Python design and public API surface.
|
||||
The initial public Python type described here is `HyperlightCodeActProvider`. Future Python backends, such as Monty, should follow the same conceptual model with their own concrete provider types rather than through a public abstract base class or a public executor parameter.
|
||||
|
||||
## What is the goal of this feature?
|
||||
|
||||
Goals:
|
||||
- Python developers can enable CodeAct through a `ContextProvider`-based integration.
|
||||
- Developers can configure a provider-owned CodeAct tool set that is separate from the agent's direct `tools=` surface.
|
||||
- Developers can use the same `execute_code` concept for both tool-enabled CodeAct and a standard code interpreter tool implementation.
|
||||
- Developers can swap execution backends over time, starting with Hyperlight while keeping room for alternatives such as Pydantic's Monty.
|
||||
- Developers can configure execution capabilities such as workspace mounts and outbound network allow lists in a portable way.
|
||||
|
||||
Success Metric:
|
||||
- Python samples exist for both a tool-enabled CodeAct mode and a standard interpreter mode.
|
||||
|
||||
Implementation-free outcome:
|
||||
- A Python developer can attach a backend-specific CodeAct provider, choose which tools are available inside CodeAct, and configure execution capabilities without rewriting the function invocation loop.
|
||||
|
||||
## What is the problem being solved?
|
||||
|
||||
The cross-SDK problem statement and decision rationale live in the [ADR](../../decisions/0024-codeact-integration.md). The items below narrow that statement to Python-specific design concerns:
|
||||
|
||||
- Today, the easiest way to prototype CodeAct is to infer or reshape the agent's direct tool surface, which is fragile and hard to reason about.
|
||||
- In Python, inferring a CodeAct tool surface from generic agent tool configuration is fragile and hard to reason about.
|
||||
- There is no first-class Python design that simultaneously covers Hyperlight-backed CodeAct now, future backend-specific providers such as Monty, and both tool-enabled and interpreter modes.
|
||||
- Sandbox capabilities such as mounted file access and outbound network access need a portable configuration model instead of ad hoc backend-specific wiring.
|
||||
- Approval behavior needs to be explicit and configurable, especially when CodeAct and direct tool calling may both be available.
|
||||
|
||||
## API Changes
|
||||
|
||||
### CodeAct contract
|
||||
|
||||
#### Terminology
|
||||
|
||||
- **CodeAct** is the primary term.
|
||||
- **Code mode**, **codemode**, and **programmatic tool calling** refer to the same concept in this document.
|
||||
- `execute_code` is the model-facing tool name used by the initial Python providers in this spec.
|
||||
|
||||
#### Provider-owned CodeAct tool registry
|
||||
|
||||
A concrete Python CodeAct provider owns the set of tools available through `call_tool(...)` inside CodeAct.
|
||||
|
||||
Rules:
|
||||
- Only tools explicitly configured on the concrete provider instance are available inside CodeAct.
|
||||
- The provider must not infer its CodeAct-managed tool set from the agent's direct `tools=` configuration.
|
||||
- Exclusive versus mixed behavior is achieved by where tools are configured, not by rewriting the agent's direct tool list.
|
||||
|
||||
Implications:
|
||||
- **CodeAct-only tool**: configured on the concrete CodeAct provider only.
|
||||
- **Direct-only tool**: configured on the agent only.
|
||||
- **Tool available both ways**: configured on both the agent and the concrete CodeAct provider.
|
||||
|
||||
#### Managing tools and capabilities after provider construction
|
||||
|
||||
There is no separate runtime setup object in the Python design. CodeAct tools, file mounts, and outbound network allow-list state are managed directly on the provider through CRUD-style registry methods.
|
||||
|
||||
Preferred pattern:
|
||||
- `add_tools(...) -> None`
|
||||
- `get_tools() -> Sequence[ToolTypes]`
|
||||
- `remove_tool(...) -> None`
|
||||
- `clear_tools() -> None`
|
||||
- `add_file_mounts(...) -> None`
|
||||
- `get_file_mounts() -> Sequence[FileMount]`
|
||||
- `remove_file_mount(...) -> None`
|
||||
- `clear_file_mounts() -> None`
|
||||
- `add_allowed_domains(...) -> None`
|
||||
- `get_allowed_domains() -> Sequence[AllowedDomain]`
|
||||
- `remove_allowed_domain(...) -> None`
|
||||
- `clear_allowed_domains() -> None`
|
||||
|
||||
Requirements:
|
||||
- The provider-owned CodeAct tool registry is keyed by tool name.
|
||||
- `add_tools(...)` adds new tools and replaces an existing provider-owned registration when the same tool name is added again.
|
||||
- `get_tools()` returns the provider's current configured CodeAct tool registry.
|
||||
- `remove_tool(...)` removes provider-owned CodeAct tools by name.
|
||||
- `clear_tools()` removes all provider-owned CodeAct tools.
|
||||
- File mounts are keyed by sandbox mount path.
|
||||
- `add_file_mounts(...)` adds new file mounts and replaces an existing mount when the same mount path is added again.
|
||||
- `get_file_mounts()` returns the provider's current configured file mounts.
|
||||
- `remove_file_mount(...)` removes file mounts by mount path.
|
||||
- `clear_file_mounts()` removes all configured file mounts.
|
||||
- Allowed domains are keyed by normalized target string.
|
||||
- `add_allowed_domains(...)` adds allow-list entries and replaces an existing entry when the same target is added again.
|
||||
- `get_allowed_domains()` returns the current outbound allow-list entries.
|
||||
- `remove_allowed_domain(...)` removes allow-list entries by target.
|
||||
- `clear_allowed_domains()` removes all configured allow-list entries.
|
||||
- Tool, file-mount, and network-allow-list mutations affect subsequent runs only; runs already in progress keep the snapshot captured at run start.
|
||||
- The provider must snapshot its effective tool registry and capability state at the start of each run so concurrent execution remains deterministic.
|
||||
|
||||
#### Approval model
|
||||
|
||||
The initial Python design follows the ADR's initial approval decision and reuses the existing tool approval vocabulary from `agent_framework._tools`:
|
||||
|
||||
- `approval_mode="always_require"`
|
||||
- `approval_mode="never_require"`
|
||||
|
||||
The provider exposes a default `approval_mode` for `execute_code`.
|
||||
|
||||
Effective `execute_code` approval is computed as follows:
|
||||
|
||||
- If the provider default is `always_require`, `execute_code` requires approval.
|
||||
- If the provider default is `never_require`, the provider evaluates the provider-owned CodeAct tool registry snapshot for that run.
|
||||
- If every provider-owned CodeAct tool in that snapshot is `never_require`, `execute_code` is `never_require`.
|
||||
- If any provider-owned CodeAct tool in that snapshot is `always_require`, `execute_code` is `always_require`, even if the generated code may not call that tool.
|
||||
- Provider-owned tool calls made through `call_tool(...)` during that execution run use the approval already determined for `execute_code`.
|
||||
- Direct-only agent tools are excluded from this calculation.
|
||||
- File and network capabilities do not create a separate runtime approval check in the initial model; configuring them on the provider, including adding file mounts or outbound network allow-list entries, is itself the approval for those capabilities.
|
||||
|
||||
This is intentionally conservative and matches the shape of the current function-tool approval flow, where `FunctionTool` uses `always_require` / `never_require` and the auto-invocation loop escalates the whole batch if any called tool requires approval.
|
||||
|
||||
If one sensitive provider-owned tool causes `execute_code` to require approval more often than desired, the mitigation is to keep that tool direct-only or expose it through a different CodeAct provider/tool surface. The initial model does not try to infer whether generated code will actually call that tool before approval.
|
||||
|
||||
If the framework later standardizes pre-execution inspection or nested per-tool approvals, the Python provider surface can grow to expose that explicitly. The initial design does not assume that those extra modes are required.
|
||||
|
||||
#### Shared execution flow
|
||||
|
||||
On each run:
|
||||
1. Resolve the provider's backend/runtime behavior, capabilities, provider default `approval_mode`, and provider-owned tool registry.
|
||||
2. Compute the effective approval requirement for `execute_code` from the provider default plus the provider-owned tool registry snapshot.
|
||||
3. Build provider-defined instructions.
|
||||
4. Add `execute_code` to the model-facing tool surface.
|
||||
5. Invoke the underlying model.
|
||||
6. When `execute_code` is called, create or reuse an execution environment keyed by provider type, backend setup identity, capability configuration, and provider-owned tool signature.
|
||||
7. If the current provider mode exposes host tools, expose `call_tool(...)` bound only to the provider-owned tool registry.
|
||||
8. Execute code and convert results to framework-native content objects.
|
||||
|
||||
Caching rules:
|
||||
- Backends that support snapshots may cache a reusable clean snapshot.
|
||||
- Backends that do not support snapshots may still cache warm initialization artifacts.
|
||||
- No mutable per-run execution state may be shared across concurrent runs.
|
||||
- In-memory interpreter state does not persist across separate `execute_code` calls.
|
||||
- Configured workspace files, mounted files, and any writable artifact/output area are the supported persistence mechanism across calls when the backend exposes them.
|
||||
|
||||
### Python public API
|
||||
|
||||
#### Core types
|
||||
|
||||
```python
|
||||
class FileMount(NamedTuple):
|
||||
host_path: str | Path
|
||||
mount_path: str
|
||||
|
||||
FileMountInput = str | tuple[str | Path, str] | FileMount
|
||||
|
||||
|
||||
class AllowedDomain(NamedTuple):
|
||||
target: str
|
||||
methods: tuple[str, ...] | None = None
|
||||
|
||||
|
||||
AllowedDomainInput = str | tuple[str, str | Sequence[str]] | AllowedDomain
|
||||
|
||||
|
||||
class HyperlightCodeActProvider(ContextProvider):
|
||||
def __init__(
|
||||
self,
|
||||
source_id: str = "hyperlight_codeact",
|
||||
*,
|
||||
backend: str = "wasm",
|
||||
module: str | None = "python_guest.path",
|
||||
module_path: str | None = None,
|
||||
tools: ToolTypes | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] = "never_require",
|
||||
workspace_root: Path | None = None,
|
||||
file_mounts: Sequence[FileMountInput] = (),
|
||||
allowed_domains: Sequence[AllowedDomainInput] = (),
|
||||
) -> None: ...
|
||||
|
||||
def add_tools(self, tools: ToolTypes | Sequence[ToolTypes]) -> None: ...
|
||||
def get_tools(self) -> Sequence[ToolTypes]: ...
|
||||
def remove_tool(self, name: str) -> None: ...
|
||||
def clear_tools(self) -> None: ...
|
||||
def add_file_mounts(self, mounts: FileMountInput | Sequence[FileMountInput]) -> None: ...
|
||||
def get_file_mounts(self) -> Sequence[FileMount]: ...
|
||||
def remove_file_mount(self, mount_path: str) -> None: ...
|
||||
def clear_file_mounts(self) -> None: ...
|
||||
def add_allowed_domains(self, domains: AllowedDomainInput | Sequence[AllowedDomainInput]) -> None: ...
|
||||
def get_allowed_domains(self) -> Sequence[AllowedDomain]: ...
|
||||
def remove_allowed_domain(self, domain: str) -> None: ...
|
||||
def clear_allowed_domains(self) -> None: ...
|
||||
```
|
||||
|
||||
`file_mounts` accepts three equivalent input forms:
|
||||
- `"data/report.csv"` uses the same relative path on the host and in the sandbox.
|
||||
- `("fixtures/users.json", "data/users.json")` or `(Path("fixtures/users.json"), "data/users.json")` uses distinct host and sandbox paths.
|
||||
- `FileMount(Path("fixtures/users.json"), "data/users.json")` is the named-tuple form of the explicit pair.
|
||||
|
||||
`allowed_domains` accepts three equivalent input forms:
|
||||
- `"github.com"` allows that target with all backend-supported methods.
|
||||
- `("github.com", "GET")` or `("github.com", ["GET", "HEAD"])` uses an explicit per-target method list.
|
||||
- `AllowedDomain("github.com", ("GET", "HEAD"))` is the named-tuple form of the explicit entry.
|
||||
|
||||
No public abstract `CodeActContextProvider` base or public `executor=` parameter is required for the initial Python API.
|
||||
|
||||
The initial alpha package also exports a standalone `HyperlightExecuteCodeTool`
|
||||
for direct-tool scenarios where a provider is not needed. That standalone tool
|
||||
should advertise `call_tool(...)`, the registered sandbox tools, and capability
|
||||
state through its own `description` rather than requiring separate agent
|
||||
instructions.
|
||||
|
||||
Provider modes:
|
||||
- If no CodeAct-managed tools are configured, `HyperlightCodeActProvider` uses interpreter-style behavior.
|
||||
- If one or more CodeAct-managed tools are configured, `HyperlightCodeActProvider` uses tool-enabled behavior.
|
||||
|
||||
#### Python provider implementation contract
|
||||
|
||||
The concrete provider plugs into the existing Python `ContextProvider` surface from `agent_framework._sessions`.
|
||||
|
||||
The Hyperlight package also depends on a small set of core hooks that must remain available from `agent-framework-core`:
|
||||
- `ContextProvider.before_run(...)`
|
||||
- `SessionContext.extend_instructions(...)`
|
||||
- `SessionContext.extend_tools(...)`
|
||||
- per-run runtime tool access via `SessionContext.options["tools"]`
|
||||
- the shared `ApprovalMode` vocabulary used by `FunctionTool`
|
||||
|
||||
Required lifecycle hook:
|
||||
- `before_run(*, agent, session, context, state) -> None`
|
||||
|
||||
Optional lifecycle hook:
|
||||
- `after_run(*, agent, session, context, state) -> None`
|
||||
|
||||
`before_run(...)` is responsible for:
|
||||
- snapshotting the current CodeAct-managed tool registry and capability settings for the run,
|
||||
- computing the effective approval requirement for `execute_code` from the provider default and the snapshotted tool registry,
|
||||
- adding a short CodeAct guidance block,
|
||||
- adding `execute_code` to the run through `SessionContext.extend_tools(...)`,
|
||||
- and wiring any backend-specific execution state needed for the run.
|
||||
|
||||
These steps run on every invocation rather than once at construction time because the provider supports CRUD mutations between runs, concurrent runs need independent snapshots, and the effective approval and instructions depend on the tool registry state captured at run start. When the tool registry and capability configuration are fixed for the lifetime of the agent, the manual wiring pattern (see `codeact_manual_wiring.py`) can be used instead, which passes the tool and instructions directly to the `Agent` constructor and avoids the per-run provider lifecycle entirely.
|
||||
|
||||
If the provider stores anything in `state`, that value must stay JSON-serializable.
|
||||
|
||||
Mutating the provider after `before_run(...)` has captured a run-scoped snapshot is allowed, but it affects subsequent runs only. Provider implementations should synchronize state capture and CRUD operations so shared provider instances remain safe across concurrent runs.
|
||||
|
||||
`after_run(...)` is responsible for any backend-specific cleanup or post-processing that must happen after the model invocation completes.
|
||||
|
||||
If shared internal helpers are introduced later for multiple concrete providers, they should standardize responsibilities for:
|
||||
- building instructions,
|
||||
- computing effective approval,
|
||||
- configuring file access,
|
||||
- configuring network access,
|
||||
- preparing or restoring execution state,
|
||||
- executing code,
|
||||
- and converting backend output into framework-native `Content`.
|
||||
|
||||
#### Runtime behavior
|
||||
|
||||
- `before_run(...)` adds a short CodeAct guidance block through `SessionContext.extend_instructions(...)`.
|
||||
- `before_run(...)` adds `execute_code` through `SessionContext.extend_tools(...)`.
|
||||
- The detailed `call_tool(...)`, sandbox-tool, and capability guidance is carried by `execute_code.description`.
|
||||
- `execute_code` invokes the configured Hyperlight sandbox guest.
|
||||
- If the current CodeAct tool registry is non-empty, the runtime injects `call_tool(...)` bound to the provider-owned tool registry.
|
||||
- The provider does not inspect or mutate `Agent.default_options["tools"]` or `context.options["tools"]` to determine its CodeAct tool set.
|
||||
- The provider snapshots the current CodeAct tool registry and capability state at run start, so later registry and allow-list mutations only affect future runs.
|
||||
- Interpreter versus tool-enabled behavior is derived from the concrete provider and the presence of CodeAct-managed tools, not from a separate public profile object.
|
||||
- `execute_code` should be traced like a normal tool invocation within the surrounding agent run, and provider-owned tool calls executed through `call_tool(...)` should continue to emit ordinary tool invocation telemetry.
|
||||
|
||||
#### Backend integration
|
||||
|
||||
Initial public provider:
|
||||
- `HyperlightCodeActProvider`
|
||||
|
||||
Backend-specific notes:
|
||||
- **Hyperlight**
|
||||
- Provider construction needs a guest artifact via `module`, which may be a packaged guest module name or a path to a compiled guest artifact.
|
||||
- File access maps naturally to Hyperlight Sandbox's read-only `/input` and writable `/output` capability model.
|
||||
- Network access is denied by default and is enabled through per-target allow-list entries.
|
||||
- **Monty**
|
||||
- A future `MontyCodeActProvider` should be a separate public type rather than a `HyperlightCodeActProvider` mode.
|
||||
- Monty does not expose built-in filesystem or network access directly inside the interpreter.
|
||||
- File and URL access are mediated through host-provided external functions, so a Monty provider would need to translate provider settings into virtual files and allow-checked callbacks.
|
||||
- Monty setup may also include backend-specific inputs such as `script_name`, optional type-check stubs, or restored snapshots.
|
||||
|
||||
#### Capability handling
|
||||
|
||||
Capabilities are first-class `HyperlightCodeActProvider` init parameters and provider-managed CRUD surfaces:
|
||||
- `workspace_root`
|
||||
- `file_mounts`
|
||||
- `allowed_domains`
|
||||
|
||||
Concrete providers should normalize these settings internally. Hyperlight can map them directly to sandbox capabilities, while Monty must enforce them through host-mediated file and network functions and may apply stricter URL-level checks than the public provider surface expresses.
|
||||
|
||||
Expected management split:
|
||||
- `workspace_root` remains a direct configuration value on the provider,
|
||||
- file mounts are managed through provider CRUD methods,
|
||||
- outbound allow-list entries are managed through provider CRUD methods.
|
||||
|
||||
Enabling access means:
|
||||
- Configuring `workspace_root` or any `file_mounts` enables the sandbox filesystem surface exposed through `/input` and `/output`.
|
||||
- Leaving both `workspace_root` and `file_mounts` unset means no filesystem surface is configured.
|
||||
- Adding any `allowed_domains` entry enables outbound access only for the configured targets; leaving it empty means network access is disabled without a separate `network_mode` flag.
|
||||
- A string target allows all backend-supported methods for that target; an explicit tuple or `AllowedDomain` entry narrows the methods for that target.
|
||||
|
||||
Backends may implement stricter semantics than these top-level settings. For example, Hyperlight naturally maps file access to `/input` and `/output`, while Monty would enforce equivalent policy through host-provided callbacks rather than direct interpreter I/O.
|
||||
|
||||
#### Execution output representation
|
||||
|
||||
Backend execution output should be translated into existing AF `Content` values rather than a custom `CodeActExecutionResult` type.
|
||||
|
||||
Use the existing content model from `agent_framework._types`, for example:
|
||||
- `Content.from_code_interpreter_tool_result(outputs=[...])` to surface the overall result of sandboxed code execution,
|
||||
- `Content.from_text(...)` for plain textual output,
|
||||
- `Content.from_data(...)` or `Content.from_uri(...)` for generated files or binary artifacts,
|
||||
- `Content.from_error(...)` for execution failures,
|
||||
- and `Content.from_function_result(..., result=list[Content])` when surfacing the final result of `execute_code` through the normal tool result path.
|
||||
|
||||
#### `execute_code` input contract
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Code to execute using the provider's configured backend/runtime behavior."
|
||||
}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
```
|
||||
|
||||
Execution failures should surface readable error text and structured error `Content`, not a custom backend result object.
|
||||
|
||||
Timeouts, out-of-memory conditions, backend crashes, and similar sandbox failures are all `execute_code` failures and should surface as structured error content. Partial textual or file outputs may be returned only when the backend can report them unambiguously; callers should not rely on partial-output recovery as a portable contract.
|
||||
|
||||
## E2E Code Samples
|
||||
|
||||
### Tool-enabled CodeAct mode
|
||||
|
||||
```python
|
||||
codeact = HyperlightCodeActProvider(
|
||||
tools=[fetch_docs, query_data],
|
||||
workspace_root="./workdir",
|
||||
allowed_domains=[("api.github.com", "GET")],
|
||||
)
|
||||
codeact.add_tools([lookup_user])
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="assistant",
|
||||
tools=[send_email], # direct-only tool
|
||||
context_providers=[codeact],
|
||||
)
|
||||
```
|
||||
|
||||
### Standard code interpreter mode
|
||||
|
||||
```python
|
||||
codeact = HyperlightCodeActProvider(
|
||||
workspace_root="./data",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="interpreter",
|
||||
context_providers=[codeact],
|
||||
)
|
||||
```
|
||||
|
||||
### Manual static wiring (no per-run provider lifecycle)
|
||||
|
||||
When the tool registry and capability configuration are fixed, the provider lifecycle can be skipped entirely. Build the `execute_code` tool and instructions once and pass them directly to the agent:
|
||||
|
||||
```python
|
||||
execute_code = HyperlightExecuteCodeTool(
|
||||
tools=[fetch_docs, query_data],
|
||||
workspace_root="./workdir",
|
||||
allowed_domains=[("api.github.com", "GET")],
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
codeact_instructions = execute_code.build_instructions(tools_visible_to_model=False)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="assistant",
|
||||
instructions=f"You are a helpful assistant.\n\n{codeact_instructions}",
|
||||
tools=[send_email, execute_code],
|
||||
)
|
||||
```
|
||||
@@ -1,48 +0,0 @@
|
||||
# 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,239 +0,0 @@
|
||||
# Durable agents
|
||||
|
||||
## Overview
|
||||
|
||||
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)
|
||||
@@ -1,390 +0,0 @@
|
||||
# Vector Stores and Embeddings
|
||||
|
||||
## Overview
|
||||
|
||||
This feature ports the vector store abstractions, embedding generator abstractions, and their implementations from Semantic Kernel into Agent Framework. The ported code follows AF's coding standards, feels native to AF, and is structured to allow data models/schemas to be reusable across both frameworks. The embedding abstraction combines the best of SK's `EmbeddingGeneratorBase` and MEAI's `IEmbeddingGenerator<TInput, TEmbedding>`.
|
||||
|
||||
| Capability | Description |
|
||||
| --- | --- |
|
||||
| Embedding generation | Generic embedding client abstraction supporting text, image, and audio inputs |
|
||||
| Vector store collections | CRUD operations on vector store collections (upsert, get, delete) |
|
||||
| Vector search | Unified search interface with `search_type` parameter (`"vector"`, `"keyword_hybrid"`) |
|
||||
| Data model decorator | `@vectorstoremodel` decorator for defining vector store data models (supports Pydantic, dataclasses, plain classes, dicts) |
|
||||
| Agent tools | `create_search_tool`, `create_upsert_tool`, `create_get_tool`, `create_delete_tool` for agent-usable vector store operations |
|
||||
| In-memory store | Zero-dependency vector store for testing and development |
|
||||
| 13+ connectors | Azure AI Search, Qdrant, Redis, PostgreSQL, MongoDB, Cosmos DB, Pinecone, Chroma, Weaviate, Oracle, SQL Server, FAISS |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Embedding Abstractions (combining SK + MEAI)
|
||||
- **Both Protocol and Base class** (matching AF's `SupportsChatGetResponse` + `BaseChatClient` pattern):
|
||||
- `SupportsGetEmbeddings` — Protocol for duck-typing
|
||||
- `BaseEmbeddingClient` — ABC base class for implementations (similar to `BaseChatClient`)
|
||||
- **Generic input type** (`EmbeddingInputT`, default `str`) from MEAI — allows image/audio embeddings in the future
|
||||
- **Generic output type** (`EmbeddingT`, default `list[float]`) from MEAI — supports `list[float]`, `list[int]`, `bytes`, etc.
|
||||
- **Generic order**: `[EmbeddingInputT, EmbeddingT, EmbeddingOptionsT]` — options last, matching MEAI's `IEmbeddingGenerator<TInput, TEmbedding>` with options appended
|
||||
- **TypeVar naming convention**: Use `SuffixT` per AF standard (e.g., `EmbeddingInputT`, `EmbeddingT`, `ModelT`, `KeyT`)
|
||||
- `EmbeddingGenerationOptions` TypedDict (inspired by MEAI, matching AF's `ChatOptions` pattern) — `total=False`, includes `dimensions`, `model_id`. No `additional_properties` since each implementation extends with its own fields.
|
||||
- Protocol and base class are generic over input, output, and options: `SupportsGetEmbeddings[EmbeddingInputT, EmbeddingT, OptionsContraT]`, `BaseEmbeddingClient[EmbeddingInputT, EmbeddingT, OptionsCoT]`
|
||||
- **`Embedding[EmbeddingT]` type** in `_types.py` — a lightweight generic class (not Pydantic) with `vector: EmbeddingT`, `model_id: str | None`, `dimensions: int | None` (explicit or computed from vector), `created_at: datetime | None`, `additional_properties: dict[str, Any]`
|
||||
- **`GeneratedEmbeddings[EmbeddingT, EmbeddingOptionsT]` type** — a list-like container of `Embedding[EmbeddingT]` objects with `options: EmbeddingOptionsT | None` (stores the options used to generate), `usage: dict[str, Any] | None`, `additional_properties: dict[str, Any]`
|
||||
- **No numpy dependency** — return `list[float]` by default; users cast as needed
|
||||
|
||||
### Vector Store Abstractions
|
||||
- **Port core abstractions without Pydantic for internal classes** — use plain classes
|
||||
- **Both Protocol and Base class** for vector store operations (matching AF pattern):
|
||||
- `SupportsVectorUpsert` / `SupportsVectorSearch` — Protocols for duck-typing (follows `Supports<Capability>` naming convention)
|
||||
- `BaseVectorCollection` / `BaseVectorSearch` — ABC base classes for implementations
|
||||
- `BaseVectorStore` — ABC base class for store operations (factory for collections, no protocol needed)
|
||||
- **TypeVar naming convention**: `ModelT`, `KeyT`, `FilterT` (suffix T, per AF standard)
|
||||
- **Support Pydantic for user-facing data models** — the `@vectorstoremodel` decorator and `VectorStoreCollectionDefinition` should work with Pydantic models, dataclasses, plain classes, and dicts
|
||||
- **Remove SK-specific dependencies** — no `KernelBaseModel`, `KernelFunction`, `KernelParameterMetadata`, `kernel_function`, `PromptExecutionSettings`
|
||||
- **Embedding types in `_types.py`**, embedding protocol/base class in `_clients.py`
|
||||
- **All vector store specific types, enums, protocols, base classes** in `_vectors.py`
|
||||
- **Error handling** uses AF's exception hierarchy (e.g., `IntegrationException` variants)
|
||||
|
||||
### Package Structure
|
||||
- **Embedding types** (`Embedding`, `GeneratedEmbeddings`, `EmbeddingGenerationOptions`) in `agent_framework/_types.py`
|
||||
- **Embedding protocol + base class** (`SupportsGetEmbeddings`, `BaseEmbeddingClient`) in `agent_framework/_clients.py`
|
||||
- **All vector store specific code** in a new `agent_framework/_vectors.py` module — this includes:
|
||||
- Enums: `FieldTypes`, `IndexKind`, `DistanceFunction`
|
||||
- `VectorStoreField`, `VectorStoreCollectionDefinition`
|
||||
- `SearchOptions`, `SearchResponse`, `RecordFilterOptions`
|
||||
- `@vectorstoremodel` decorator
|
||||
- Serialization/deserialization protocols
|
||||
- `VectorStoreRecordHandler`, `BaseVectorCollection`, `BaseVectorStore`, `BaseVectorSearch`
|
||||
- `SupportsVectorUpsert`, `SupportsVectorSearch` protocols
|
||||
- **OpenAI embeddings** in `agent_framework/openai/` (built into core, like OpenAI chat)
|
||||
- **Azure OpenAI embeddings** in `agent_framework/azure/` (built into core, follows `AzureOpenAIChatClient` pattern)
|
||||
- **Each vector store connector** in its own AF package under `packages/`
|
||||
- **In-memory store** in core (no external deps)
|
||||
- **TextSearch and its implementations** (Brave, Google) — last phase, separate work
|
||||
|
||||
## Naming: SK → AF
|
||||
|
||||
### Names that change
|
||||
|
||||
| SK Name | AF Name | Rationale |
|
||||
|---------|---------|-----------|
|
||||
| `VectorStoreCollection` | `BaseVectorCollection` | Drop redundant `Store`, add `Base` prefix per AF pattern |
|
||||
| `VectorStore` | `BaseVectorStore` | Add `Base` prefix per AF pattern |
|
||||
| `VectorSearch` | `BaseVectorSearch` | Add `Base` prefix per AF pattern |
|
||||
| `VectorSearchOptions` | `SearchOptions` | Shorter — context is already vector search |
|
||||
| `VectorSearchResult` | `SearchResponse` | Align with `ChatResponse`/`AgentResponse` |
|
||||
| `GetFilteredRecordOptions` | `RecordFilterOptions` | Shorter, more natural |
|
||||
| `EmbeddingGeneratorBase` | `BaseEmbeddingClient` | Matches AF `BaseChatClient` pattern |
|
||||
| `VectorStoreCollectionProtocol` | `SupportsVectorUpsert` | AF `Supports*` naming convention |
|
||||
| `VectorSearchProtocol` | `SupportsVectorSearch` | AF `Supports*` naming convention |
|
||||
| `__kernel_vectorstoremodel__` | `__vectorstoremodel__` | Drop SK `kernel` prefix |
|
||||
| `__kernel_vectorstoremodel_definition__` | `__vectorstoremodel_definition__` | Drop SK `kernel` prefix |
|
||||
| `search()` + `hybrid_search()` | `search(search_type=...)` | Single method with `Literal` parameter |
|
||||
| `SearchType` enum | `Literal["vector", "keyword_hybrid"]` | No enum, just a literal |
|
||||
| `KernelSearchResults` | `SearchResults` | Drop SK `Kernel` prefix (plural — container of `SearchResponse` items) |
|
||||
|
||||
### Names that stay the same
|
||||
|
||||
| Name | Location |
|
||||
|------|----------|
|
||||
| `@vectorstoremodel` | `_vectors.py` |
|
||||
| `VectorStoreField` | `_vectors.py` |
|
||||
| `VectorStoreCollectionDefinition` | `_vectors.py` |
|
||||
| `VectorStoreRecordHandler` | `_vectors.py` |
|
||||
| `FieldTypes` | `_vectors.py` |
|
||||
| `IndexKind` | `_vectors.py` |
|
||||
| `DistanceFunction` | `_vectors.py` |
|
||||
| `DISTANCE_FUNCTION_DIRECTION_HELPER` | `_vectors.py` |
|
||||
| `Embedding` | `_types.py` |
|
||||
| `GeneratedEmbeddings` | `_types.py` |
|
||||
| `EmbeddingGenerationOptions` | `_types.py` |
|
||||
| `SupportsGetEmbeddings` | `_clients.py` |
|
||||
|
||||
### New AF-only names (no SK equivalent)
|
||||
|
||||
| Name | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| `BaseEmbeddingClient` | `_clients.py` | ABC base for embedding implementations |
|
||||
| `EmbeddingInputT` | `_types.py` | TypeVar for generic embedding input (default `str`) |
|
||||
| `EmbeddingTelemetryLayer` | `observability.py` | MRO-based OTel tracing for embeddings |
|
||||
| `SupportsVectorUpsert` | `_vectors.py` | Protocol for collection CRUD |
|
||||
| `SupportsVectorSearch` | `_vectors.py` | Protocol for vector search |
|
||||
| `create_search_tool` | `_vectors.py` | Creates AF `FunctionTool` from vector search |
|
||||
|
||||
## Source Files Reference (SK → AF mapping)
|
||||
|
||||
### SK Source Files
|
||||
| SK File | Lines | Content |
|
||||
|---------|-------|---------|
|
||||
| `data/vector.py` | 2369 | All vector store abstractions, enums, decorator, search |
|
||||
| `data/_shared.py` | 184 | SearchOptions, KernelSearchResults, shared search types |
|
||||
| `data/text_search.py` | 349 | TextSearch base, TextSearchResult |
|
||||
| `connectors/ai/embedding_generator_base.py` | 50 | EmbeddingGeneratorBase ABC |
|
||||
| `connectors/in_memory.py` | 520 | InMemoryCollection, InMemoryStore |
|
||||
| `connectors/azure_ai_search.py` | 793 | Azure AI Search collection + store |
|
||||
| `connectors/azure_cosmos_db.py` | 1104 | Cosmos DB (Mongo + NoSQL) |
|
||||
| `connectors/redis.py` | 845 | Redis (Hashset + JSON) |
|
||||
| `connectors/qdrant.py` | 653 | Qdrant collection + store |
|
||||
| `connectors/postgres.py` | 987 | PostgreSQL collection + store |
|
||||
| `connectors/mongodb.py` | 633 | MongoDB Atlas collection + store |
|
||||
| `connectors/pinecone.py` | 691 | Pinecone collection + store |
|
||||
| `connectors/chroma.py` | 484 | Chroma collection + store |
|
||||
| `connectors/faiss.py` | 278 | FAISS (extends InMemory) |
|
||||
| `connectors/weaviate.py` | 804 | Weaviate collection + store |
|
||||
| `connectors/oracle.py` | 1267 | Oracle collection + store |
|
||||
| `connectors/sql_server.py` | 1132 | SQL Server collection + store |
|
||||
| `connectors/ai/open_ai/services/open_ai_text_embedding.py` | 91 | OpenAI embedding impl |
|
||||
| `connectors/ai/open_ai/services/open_ai_text_embedding_base.py` | 78 | OpenAI embedding base |
|
||||
| `connectors/brave.py` | ~200 | Brave TextSearch impl |
|
||||
| `connectors/google_search.py` | ~200 | Google TextSearch impl |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Embedding Abstractions & OpenAI Implementation ✅ DONE
|
||||
**Goal:** Establish the embedding generator abstraction and ship one working implementation.
|
||||
**Mergeable:** Yes — adds new types/protocols, no breaking changes.
|
||||
**Status:** Merged via PR #4153. Closes sub-issue #4163.
|
||||
|
||||
#### 1.1 — Embedding types in `_types.py`
|
||||
- `EmbeddingInputT` TypeVar (default `str`) — generic input type for embedding generation
|
||||
- `EmbeddingT` TypeVar (default `list[float]`) — generic output embedding vector type
|
||||
- `Embedding[EmbeddingT]` generic class: `vector: EmbeddingT`, `model_id: str | None`, `dimensions: int | None` (explicit param or computed from vector length), `created_at: datetime | None`, `additional_properties: dict[str, Any]`
|
||||
- `GeneratedEmbeddings[EmbeddingT, EmbeddingOptionsT]` generic class: list-like container of `Embedding[EmbeddingT]` objects with `options: EmbeddingOptionsT | None` (the options used to generate), `usage: dict[str, Any] | None`, `additional_properties: dict[str, Any]`
|
||||
- `EmbeddingGenerationOptions` TypedDict (`total=False`): `dimensions: int`, `model_id: str` — follows the same pattern as `ChatOptions`. No `additional_properties` needed since it's a TypedDict and each implementation can extend with its own fields.
|
||||
|
||||
#### 1.2 — Embedding generator protocol + base class in `_clients.py`
|
||||
- `SupportsGetEmbeddings(Protocol[EmbeddingInputT, EmbeddingT, OptionsContraT])`: generic over input, output, and options (all with defaults), `get_embeddings(values: Sequence[EmbeddingInputT], *, options: OptionsContraT | None = None) -> Awaitable[GeneratedEmbeddings[EmbeddingT]]`
|
||||
- `BaseEmbeddingClient(ABC, Generic[EmbeddingInputT, EmbeddingT, OptionsCoT])`: ABC base class mirroring `BaseChatClient` pattern
|
||||
- `__init__` with `additional_properties`, etc.
|
||||
- Abstract `get_embeddings(...)` for subclasses to implement directly (no `_inner_*` indirection — simpler than chat, no middleware needed)
|
||||
- `EmbeddingTelemetryLayer` in `observability.py` — MRO-based telemetry (no closure), `gen_ai.operation.name = "embeddings"`
|
||||
|
||||
#### 1.3 — OpenAI embedding generator in `agent_framework/openai/` and `agent_framework/azure/`
|
||||
- `RawOpenAIEmbeddingClient` — implements `get_embeddings` via `_ensure_client()` factory
|
||||
- `OpenAIEmbeddingClient(OpenAIConfigMixin, EmbeddingTelemetryLayer[str, list[float], OptionsT], RawOpenAIEmbeddingClient[OptionsT])` — full client with config + telemetry layers
|
||||
- `OpenAIEmbeddingOptions(EmbeddingGenerationOptions)` — extends with `encoding_format`, `user`
|
||||
- `AzureOpenAIEmbeddingClient` in `agent_framework/azure/` — follows `AzureOpenAIChatClient` pattern with `AzureOpenAIConfigMixin`, `load_settings`, Entra ID credential support
|
||||
- `AzureOpenAISettings` extended with `embedding_deployment_name` (env var: `AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME`)
|
||||
|
||||
#### 1.4 — Tests and samples
|
||||
- Unit tests for types, protocol, base class, OpenAI client, Azure OpenAI client
|
||||
- Integration tests for OpenAI and Azure OpenAI (gated behind credentials check, `@pytest.mark.flaky`)
|
||||
- Samples in `samples/02-agents/embeddings/` — `openai_embeddings.py`, `azure_openai_embeddings.py`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Embedding Generators for Existing Providers
|
||||
**Goal:** Add embedding generators to all existing AF provider packages that have chat clients.
|
||||
**Mergeable:** Yes — each is independent, added to existing provider packages.
|
||||
|
||||
#### 2.1 — Foundry inference embedding (in `packages/foundry/`)
|
||||
#### 2.2 — Ollama embedding (in `packages/ollama/`)
|
||||
#### 2.3 — Anthropic embedding (in `packages/anthropic/`)
|
||||
#### 2.4 — Bedrock embedding (in `packages/bedrock/`)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Core Vector Store Abstractions
|
||||
**Goal:** Establish all vector store types, enums, the decorator, collection definition, and base classes.
|
||||
**Mergeable:** Yes — adds new abstractions, no breaking changes.
|
||||
|
||||
#### 3.1 — Vector store enums and field types in `_vectors.py`
|
||||
- `FieldTypes` enum: `KEY`, `VECTOR`, `DATA`
|
||||
- `IndexKind` enum: `HNSW`, `FLAT`, `IVF_FLAT`, `DISK_ANN`, `QUANTIZED_FLAT`, `DYNAMIC`, `DEFAULT`
|
||||
- `DistanceFunction` enum: `COSINE_SIMILARITY`, `COSINE_DISTANCE`, `DOT_PROD`, `EUCLIDEAN_DISTANCE`, `EUCLIDEAN_SQUARED_DISTANCE`, `MANHATTAN`, `HAMMING`, `DEFAULT`
|
||||
- No `SearchType` enum — use `Literal["vector", "keyword_hybrid"]` instead, per AF convention of avoiding unnecessary imports
|
||||
- `VectorStoreField` plain class (not Pydantic)
|
||||
- `VectorStoreCollectionDefinition` class (not Pydantic internally, but supports Pydantic models as input)
|
||||
- `SearchOptions` plain class — includes `score_threshold: float | None` for filtering results by score (see note below)
|
||||
- `SearchResponse` generic class
|
||||
- `RecordFilterOptions` plain class
|
||||
- `DISTANCE_FUNCTION_DIRECTION_HELPER` dict
|
||||
|
||||
#### 3.2 — `@vectorstoremodel` decorator
|
||||
- Port from SK, works with dataclasses, Pydantic models, plain classes, and dicts
|
||||
- Sets `__vectorstoremodel__` and `__vectorstoremodel_definition__` on the class
|
||||
- Remove SK-specific `kernel` prefix (`__kernel_vectorstoremodel__` → `__vectorstoremodel__`)
|
||||
|
||||
#### 3.3 — Serialization/deserialization protocols
|
||||
- `SerializeMethodProtocol`, `ToDictFunctionProtocol`, `FromDictFunctionProtocol`, etc.
|
||||
- Port the record handler logic but without Pydantic base class — use plain class or ABC
|
||||
|
||||
#### 3.4 — Vector store base classes in `_vectors.py`
|
||||
- `VectorStoreRecordHandler` — internal base class that handles serialization/deserialization between user data models and store-specific formats, plus embedding generation for vector fields. Both `BaseVectorCollection` and `BaseVectorSearch` extend this.
|
||||
- `BaseVectorCollection(VectorStoreRecordHandler)` — base for collections
|
||||
- Uses `SupportsGetEmbeddings` instead of `EmbeddingGeneratorBase`
|
||||
- Not a Pydantic model — use `__init__` with explicit params
|
||||
- `upsert`, `get`, `delete`, `ensure_collection_exists`, `collection_exists`, `ensure_collection_deleted`
|
||||
- Async context manager support
|
||||
- `BaseVectorStore` — base for stores
|
||||
- `get_collection`, `list_collection_names`, `collection_exists`, `ensure_collection_deleted`
|
||||
- Async context manager support
|
||||
|
||||
#### 3.5 — Vector search base class
|
||||
- `BaseVectorSearch(VectorStoreRecordHandler)` — base for vector search
|
||||
- Single `search(search_type=...)` method with `search_type: Literal["vector", "keyword_hybrid"]` parameter — no enum, just a literal
|
||||
- `_inner_search` abstract method for implementations
|
||||
- Filter building with lambda parser (AST-based)
|
||||
- Vector generation from values using embedding generator
|
||||
|
||||
#### 3.6 — Protocols for type checking
|
||||
- `SupportsVectorUpsert` — Protocol for upsert/get/delete operations
|
||||
- `SupportsVectorSearch` — Protocol for vector search (single `search()` with `search_type` parameter)
|
||||
- No separate `SupportsVectorHybridSearch` — search type is a parameter, not a separate capability
|
||||
- No protocol for `VectorStore` — it's a factory for collections, not a capability to duck-type against
|
||||
|
||||
#### 3.7 — Exception types
|
||||
- Add vector store exceptions under `IntegrationException` or create new branch
|
||||
- `VectorStoreException`, `VectorStoreOperationException`, `VectorSearchException`, `VectorStoreModelException`, etc.
|
||||
|
||||
#### 3.8 — `create_search_tool` on `BaseVectorSearch`
|
||||
- Method on `BaseVectorSearch` that creates an AF `FunctionTool` from the vector search
|
||||
- Wraps the single `search()` method, passing `search_type` parameter
|
||||
- Accepts: `name`, `description`, `search_type`, `top`, `skip`, `filter`, `string_mapper`
|
||||
- The tool takes a query string, vectorizes it, searches, and returns results as strings
|
||||
- Can also be a standalone factory function in `_vectors.py`
|
||||
|
||||
#### 3.9 — Tests for all vector store abstractions
|
||||
- Unit tests for enums, field types, collection definition
|
||||
- Unit tests for decorator
|
||||
- Unit tests for serialization/deserialization
|
||||
- Unit tests for record handler
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: In-Memory Vector Store
|
||||
**Goal:** Provide a zero-dependency vector store for testing and development.
|
||||
**Mergeable:** Yes — first usable vector store.
|
||||
|
||||
#### 4.1 — Port `InMemoryCollection` and `InMemoryStore` into core
|
||||
- Place in `agent_framework/_vectors.py` (alongside the abstractions)
|
||||
- Supports vector search (cosine similarity, etc.)
|
||||
- No external dependencies
|
||||
|
||||
#### 4.2 — Port FAISS extension (optional, can be separate package)
|
||||
- Extends InMemory with FAISS indexing
|
||||
|
||||
#### 4.3 — Tests and sample code
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Vector Store Connectors — Tier 1 (High Priority)
|
||||
**Goal:** Ship the most commonly used vector store connectors.
|
||||
**Mergeable:** Yes — each connector is independent.
|
||||
|
||||
Each connector follows the AF package structure:
|
||||
- New package under `packages/`
|
||||
- Own `pyproject.toml`, `tests/`, lazy loading in core
|
||||
|
||||
#### 5.1 — Azure AI Search (`packages/azure-ai-search/`)
|
||||
- May extend existing package or be new
|
||||
- `AzureAISearchCollection`, `AzureAISearchStore`
|
||||
|
||||
#### 5.2 — Qdrant (`packages/qdrant/`)
|
||||
- New package
|
||||
- `QdrantCollection`, `QdrantStore`
|
||||
|
||||
#### 5.3 — Redis (`packages/redis/`)
|
||||
- May extend existing redis package
|
||||
- `RedisCollection` (JSON + Hashset variants), `RedisStore`
|
||||
|
||||
#### 5.4 — PostgreSQL/pgvector (`packages/postgres/`)
|
||||
- New package
|
||||
- `PostgresCollection`, `PostgresStore`
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Vector Store Connectors — Tier 2
|
||||
**Goal:** Ship remaining vector store connectors.
|
||||
**Mergeable:** Yes — each connector is independent.
|
||||
|
||||
#### 6.1 — MongoDB Atlas (`packages/mongodb/`)
|
||||
#### 6.2 — Azure Cosmos DB (`packages/azure-cosmos-db/`)
|
||||
- Cosmos Mongo + Cosmos NoSQL
|
||||
#### 6.3 — Pinecone (`packages/pinecone/`)
|
||||
#### 6.4 — Chroma (`packages/chroma/`)
|
||||
#### 6.5 — Weaviate (`packages/weaviate/`)
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Vector Store Connectors — Tier 3
|
||||
**Goal:** Ship niche or less common connectors.
|
||||
**Mergeable:** Yes — each connector is independent.
|
||||
|
||||
#### 7.1 — Oracle (`packages/oracle/`)
|
||||
#### 7.2 — SQL Server (`packages/sql-server/`)
|
||||
#### 7.3 — FAISS (`packages/faiss/` or in core extending InMemory)
|
||||
|
||||
> **Note:** When implementing any SQL-based connector (PostgreSQL, SQL Server, SQLite, Cosmos DB), review the .NET MEVD changes made by @roji (Shay Rojansky) in SK for design patterns, query building, filter translation, and feature parity: https://github.com/microsoft/semantic-kernel/pulls?q=is%3Apr+author%3Aroji+is%3Aclosed
|
||||
|
||||
---
|
||||
|
||||
### Phase 8: Vector Store CRUD Tools
|
||||
**Goal:** Provide a full set of agent-usable tools for CRUD operations on vector store collections.
|
||||
**Mergeable:** Yes — adds tools without changing existing APIs.
|
||||
|
||||
#### 8.1 — `create_upsert_tool` — tool for upserting records into a collection
|
||||
#### 8.2 — `create_get_tool` — tool for retrieving records by key
|
||||
- Key-based lookup only (by primary key), not a search tool
|
||||
- Documentation must clearly distinguish this from `create_search_tool`: get_tool retrieves specific records by their known key, while search_tool performs similarity/filtered search across the collection
|
||||
- Consider if this overlaps with filtered search and document when to use which
|
||||
#### 8.3 — `create_delete_tool` — tool for deleting records by key
|
||||
#### 8.4 — Tests and samples for CRUD tools
|
||||
|
||||
---
|
||||
|
||||
### Phase 9: Additional Embedding Implementations (New Providers)
|
||||
**Goal:** Provide embedding generators for providers that don't yet have AF packages.
|
||||
**Mergeable:** Yes — each is independent, new packages.
|
||||
|
||||
#### 9.1 — HuggingFace/ONNX embedding (new package or lab)
|
||||
#### 9.2 — Mistral AI embedding (new package)
|
||||
#### 9.3 — Google AI / Vertex AI embedding (new package)
|
||||
#### 9.4 — Nvidia embedding (new package)
|
||||
|
||||
---
|
||||
|
||||
### Phase 10: TextSearch Abstractions & Implementations (Separate Work)
|
||||
**Goal:** Port text search (non-vector) abstractions and implementations.
|
||||
**Mergeable:** Yes — independent of vector stores.
|
||||
|
||||
#### 10.1 — TextSearch base class and types
|
||||
- `SearchOptions`, `SearchResponse`, `TextSearchResult`
|
||||
- `TextSearch` base class with `search()` method
|
||||
- `create_search_function()` for kernel integration (may need AF equivalent)
|
||||
|
||||
#### 10.2 — Brave Search implementation
|
||||
#### 10.3 — Google Search implementation
|
||||
#### 10.4 — Vector store text search bridge (connecting VectorSearch to TextSearch interface)
|
||||
|
||||
---
|
||||
|
||||
## Key Considerations
|
||||
|
||||
1. **No Pydantic for internal classes**: All AF internal classes should use plain classes. Pydantic is only used for user-facing input validation (e.g., vector store data models).
|
||||
|
||||
2. **Protocol + Base class**: Follow AF's pattern of both a `Protocol` for duck-typing and a `Base` ABC for implementation, matching how `SupportsChatGetResponse` + `BaseChatClient` works.
|
||||
|
||||
3. **Exception hierarchy**: Use AF's `IntegrationException` branch for vector store operations, since vector stores are external dependencies.
|
||||
|
||||
4. **`from __future__ import annotations`**: Required in all files per AF coding standard.
|
||||
|
||||
5. **No `**kwargs` escape hatches in public APIs**: For user-facing interfaces, use explicit named parameters per AF coding standard. Internal implementation details (e.g., cooperative multiple inheritance / MRO patterns) may use `**kwargs` where necessary, as long as they are not exposed in public signatures.
|
||||
|
||||
6. **Lazy loading**: Connector packages use `__getattr__` lazy loading in core provider folders.
|
||||
|
||||
7. **Reusable data models**: The `@vectorstoremodel` decorator and `VectorStoreCollectionDefinition` should be agnostic enough to work with both SK and AF. The core types (`FieldTypes`, `IndexKind`, `DistanceFunction`, `VectorStoreField`) should be identical or easily mapped.
|
||||
|
||||
8. **`create_search_tool`**: The AF-native equivalent of SK's `create_search_function`. Instead of creating a `KernelFunction`, this creates an AF `FunctionTool` (via the `@tool` decorator pattern) from a vector search. This allows agents to use vector search as a tool during conversations. Design:
|
||||
- `create_search_tool(name, description, search_type, ...)` → returns a `FunctionTool` that wraps `VectorSearch.search(search_type=...)`
|
||||
- The tool accepts a query string, performs embedding + vector search, and returns results as strings
|
||||
- Supports configurable string mappers, filter functions, top/skip defaults
|
||||
- Lives in `_vectors.py` as a method on `BaseVectorSearch` and/or as a standalone factory function
|
||||
|
||||
9. **CRUD tools**: A full set of create/read/update/delete tools for vector store collections, allowing agents to manage data in vector stores. Design:
|
||||
- `create_upsert_tool(...)` → tool for upserting records
|
||||
- `create_get_tool(...)` → tool for retrieving records by key
|
||||
- `create_delete_tool(...)` → tool for deleting records
|
||||
- These are separate from search and are placed in a later phase
|
||||
|
||||
10. **Score threshold filtering**: `SearchOptions` includes `score_threshold: float | None` to filter search results by relevance score (ref: [SK .NET PR #13501](https://github.com/microsoft/semantic-kernel/pull/13501)). The semantics depend on the distance function: for similarity functions (cosine similarity, dot product), results *below* the threshold are filtered out; for distance functions (cosine distance, euclidean), results *above* the threshold are filtered out. Use `DISTANCE_FUNCTION_DIRECTION_HELPER` to determine direction. Connectors should implement this natively where the database supports it, falling back to client-side post-filtering otherwise.
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
---
|
||||
name: build-and-test
|
||||
description: How to build and test .NET projects in the Agent Framework repository. Use this when verifying or testing changes.
|
||||
---
|
||||
|
||||
- Only **UnitTest** projects need to be run locally; IntegrationTests require external dependencies.
|
||||
- See `../project-structure/SKILL.md` for project structure details.
|
||||
|
||||
## Build, Test, and Lint Commands
|
||||
|
||||
```bash
|
||||
# From dotnet/ directory
|
||||
dotnet restore --tl:off # Restore dependencies for all projects
|
||||
dotnet build --tl:off # Build all projects
|
||||
dotnet test # Run all tests
|
||||
dotnet format # Auto-fix formatting for all projects
|
||||
|
||||
# Build/test/format a specific project (preferred for isolated/internal changes)
|
||||
dotnet build src/Microsoft.Agents.AI.<Package> --tl:off
|
||||
dotnet test --project tests/Microsoft.Agents.AI.<Package>.UnitTests
|
||||
dotnet format src/Microsoft.Agents.AI.<Package>
|
||||
|
||||
# Run a single test
|
||||
# Replace the filter values with the appropriate assembly, namespace, class, and method names for the test you want to run and use * as a wildcard elsewhere, e.g. "/*/*/HttpClientTests/GetAsync_ReturnsSuccessStatusCode"
|
||||
# Use `--ignore-exit-code 8` to avoid failing the build when no tests are found for some projects
|
||||
dotnet test --filter-query "/<assemblyFilter>/<namespaceFilter>/<classFilter>/<methodFilter>" --ignore-exit-code 8
|
||||
|
||||
# Run unit tests only
|
||||
# Use `--ignore-exit-code 8` to avoid failing the build when no tests are found for integration test projects
|
||||
dotnet test --filter-query "/*UnitTests*/*/*/*" --ignore-exit-code 8
|
||||
```
|
||||
|
||||
Use `--tl:off` when building to avoid flickering when running commands in the agent.
|
||||
|
||||
## Speeding Up Builds and Testing
|
||||
|
||||
The full solution is large. Use these shortcuts:
|
||||
|
||||
| Change type | What to do |
|
||||
|-------------|------------|
|
||||
| Isolated/Internal logic | Build only the affected project and its `*.UnitTests` project. Fix issues, then build the full solution and run all unit tests. |
|
||||
| Public API surface | Build the full solution and run all unit tests immediately. |
|
||||
|
||||
Example: Building a single code project for all target frameworks
|
||||
|
||||
```bash
|
||||
# From dotnet/ directory
|
||||
dotnet build ./src/Microsoft.Agents.AI.Abstractions
|
||||
```
|
||||
|
||||
Example: Building a single code project for just .NET 10.
|
||||
|
||||
```bash
|
||||
# From dotnet/ directory
|
||||
dotnet build ./src/Microsoft.Agents.AI.Abstractions -f net10.0
|
||||
```
|
||||
|
||||
Example: Running tests for a single project using .NET 10.
|
||||
|
||||
```bash
|
||||
# From dotnet/ directory
|
||||
dotnet test --project ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0
|
||||
```
|
||||
|
||||
Example: Running a single test in a specific project using .NET 10.
|
||||
Provide the full namespace, class name, and method name for the test you want to run:
|
||||
|
||||
```bash
|
||||
# From dotnet/ directory
|
||||
dotnet test --project ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 --filter-query "/*/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests/CloningConstructorCopiesProperties"
|
||||
```
|
||||
|
||||
### Multi-target framework tip
|
||||
|
||||
Most projects target multiple .NET frameworks. If the affected code does **not** use `#if` directives for framework-specific logic, pass `-f net10.0` to speed up building and testing.
|
||||
|
||||
### Package Restore tip
|
||||
|
||||
`dotnet build` will try and restore packages for all projects on each build, which can be slow.
|
||||
Unless packages have been changed, or it's the first time building the solution, add `--no-restore` to the build command to skip this step and speed up builds.
|
||||
|
||||
Just remember to run `dotnet restore` after pulling changes, making changes to project references, or when building for the first time.
|
||||
|
||||
### Testing on Linux tip
|
||||
|
||||
Unit tests target both .NET Framework as well as .NET Core. When running on Linux, only the .NET Core tests can be run, as .NET Framework is not supported on Linux.
|
||||
|
||||
To run only the .NET Core tests, use the `-f net10.0` option with `dotnet test`.
|
||||
|
||||
### Microsoft Testing Platform (MTP)
|
||||
|
||||
Tests use the [Microsoft Testing Platform](https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-intro) via xUnit v3. Key differences from the legacy VSTest runner:
|
||||
|
||||
- **`dotnet test` requires `--project`** to specify a test project directly (positional arguments are no longer supported).
|
||||
- **Test output** uses the MTP format (e.g., `[✓112/x0/↓0]` progress and `Test run summary: Passed!`).
|
||||
- **TRX reports** use `--report-xunit-trx` instead of `--logger trx`.
|
||||
- **Code coverage** uses `Microsoft.Testing.Extensions.CodeCoverage` with `--coverage --coverage-output-format cobertura`.
|
||||
- **Running a test project directly** is supported via `dotnet run --project <test-project>`. This bypasses the `dotnet test` infrastructure and runs the test executable directly with the MTP command line.
|
||||
|
||||
- **Running tests across the solution** with a filter may cause some projects to match zero tests, which MTP treats as a failure (exit code 8). Use `--ignore-exit-code 8` to suppress this:
|
||||
|
||||
```bash
|
||||
# Run all unit tests across the solution, ignoring projects with no matching tests
|
||||
dotnet test --solution ./agent-framework-dotnet.slnx --no-build -f net10.0 --ignore-exit-code 8
|
||||
```
|
||||
|
||||
- **Running tests with `--solution` for a specific TFM** requires all projects in the solution to support that TFM. Not all projects target every framework (e.g., some are `net10.0`-only). Use `./dotnet/eng/scripts/New-FilteredSolution.ps1` to generate a filtered solution:
|
||||
|
||||
```powershell
|
||||
# Generate a filtered solution for net472 and run tests
|
||||
$filtered = ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472
|
||||
dotnet test --solution $filtered --no-build -f net472 --ignore-exit-code 8
|
||||
|
||||
# Exclude samples and keep only unit test projects
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -ExcludeSamples -TestProjectNameFilter "*UnitTests*" -OutputPath dotnet/filtered-unit.slnx
|
||||
```
|
||||
|
||||
```bash
|
||||
# Run tests via dotnet test (uses MTP under the hood)
|
||||
dotnet test --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0
|
||||
|
||||
# Run tests with code coverage (Cobertura format)
|
||||
dotnet test --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 --coverage --coverage-output-format cobertura --coverage-settings ./tests/coverage.runsettings
|
||||
|
||||
# Run tests directly via dotnet run (MTP native command line)
|
||||
dotnet run --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0
|
||||
|
||||
# Show MTP command line help
|
||||
dotnet run --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 -- -?
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: project-structure
|
||||
description: Explains the project structure of the agent-framework .NET solution
|
||||
---
|
||||
|
||||
# Agent Framework .NET Project Structure
|
||||
|
||||
```
|
||||
dotnet/
|
||||
├── src/
|
||||
│ ├── Microsoft.Agents.AI/ # Core AI agent implementations
|
||||
│ ├── Microsoft.Agents.AI.Abstractions/ # Core AI agent abstractions
|
||||
│ ├── Microsoft.Agents.AI.A2A/ # Agent-to-Agent (A2A) provider
|
||||
│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider
|
||||
│ ├── Microsoft.Agents.AI.Foundry/ # Microsoft Foundry Agents (v2) provider
|
||||
│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Microsoft Foundry Agents (v1) provider
|
||||
│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider
|
||||
│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration
|
||||
│ └── ... # Other packages
|
||||
├── samples/ # Sample applications
|
||||
└── tests/ # Unit and integration tests
|
||||
```
|
||||
|
||||
## Main Folders
|
||||
|
||||
| Folder | Contents |
|
||||
|--------|----------|
|
||||
| `src/` | Source code projects |
|
||||
| `tests/` | Test projects — named `<Source-Code-Project>.UnitTests` or `<Source-Code-Project>.IntegrationTests` |
|
||||
| `samples/` | Sample projects |
|
||||
| `src/Shared`, `src/LegacySupport` | Shared code files included by multiple source code projects (see README.md files in these folders or their subdirectories for instructions on how to include them in a project) |
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
name: verify-dotnet-samples
|
||||
description: How to build, run and verify the .NET sample projects in the Agent Framework repository. Use this when a user wants to verify that the samples still function as expected.
|
||||
---
|
||||
|
||||
# Verifying .NET Sample Projects
|
||||
|
||||
## Sample Pre-requisites
|
||||
|
||||
We should only support verifying samples that:
|
||||
1. Use environment variables for configuration.
|
||||
2. Have no complex setup requirements, e.g., where multiple applications need to be run together, or where we need to launch a browser, etc.
|
||||
|
||||
Always report to the user which samples were run and which were not, and why.
|
||||
|
||||
## Verifying a sample
|
||||
|
||||
Samples should be verified to ensure that they actually work as intended and that their output matches what is expected.
|
||||
For each sample that is run, output should be produced that shows the result and explains the reasoning about what output
|
||||
was expected, what was produced, and why it didn't match what the sample was expected to produce.
|
||||
|
||||
Steps to verify a sample:
|
||||
1. Read the code for the sample
|
||||
1. Check what environment variables are required for the sample
|
||||
1. Check if each environment variable has been set
|
||||
1. If there are any missing, give the user a list of missing environment variables to set and terminate
|
||||
1. Summarize what the expected output of the sample should be
|
||||
1. Run the sample
|
||||
1. Show the user any output from the sample run as it gets produced, so that they can see the run progress
|
||||
1. Check the output of the run against expectations
|
||||
1. After running all requested samples, produce output for each sample that was verified:
|
||||
1. If expectations were matched, output the following:
|
||||
```text
|
||||
[Sample Name] Succeeded
|
||||
```
|
||||
1. If expectations were not matched, output the following:
|
||||
```text
|
||||
[Sample Name] Failed
|
||||
Actual Output:
|
||||
[What the sample produced]
|
||||
Expected Output:
|
||||
[Explanation of what was expected and why the actual output didn't match expectations]
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Most samples use environment variables to configure settings.
|
||||
|
||||
```csharp
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
```
|
||||
|
||||
To run a sample, the environment variables should be set first.
|
||||
Before running a sample, check whether each environment variable in the sample has a value and
|
||||
then give the user a list of environment variables to set.
|
||||
|
||||
You can provide the user some examples of how to set the variables like this:
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://my-openai-instance.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
To check if a variable has a value use e.g.:
|
||||
|
||||
```bash
|
||||
echo $AZURE_OPENAI_ENDPOINT
|
||||
```
|
||||
|
||||
## How to Run a Sample (General Pattern)
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/<category>/<sample-dir>
|
||||
dotnet run
|
||||
```
|
||||
|
||||
For multi-targeted projects (e.g., Durable console apps), specify the framework:
|
||||
|
||||
```bash
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
-225
@@ -1,225 +0,0 @@
|
||||
---
|
||||
name: verify-samples-tool
|
||||
description: How to use the verify-samples tool to run, verify, and manage sample definitions in the Agent Framework repository. Use this when adding, updating, or running sample verification.
|
||||
---
|
||||
|
||||
# verify-samples Tool
|
||||
|
||||
The `verify-samples` project (`dotnet/eng/verify-samples/`) is an automated tool that runs sample projects and verifies their output using deterministic checks and AI-powered verification.
|
||||
|
||||
## Running verify-samples
|
||||
|
||||
**Important:** By default, samples must be pre-built before running verify-samples. Build the solution first, or pass `--build` to build samples during the run:
|
||||
|
||||
```bash
|
||||
cd dotnet
|
||||
dotnet build agent-framework-dotnet.slnx -f net10.0
|
||||
```
|
||||
|
||||
Then run verify-samples:
|
||||
|
||||
```bash
|
||||
# Run all samples across all categories
|
||||
dotnet run --project eng/verify-samples -- --log results.log --csv results.csv
|
||||
|
||||
# Run a specific category
|
||||
dotnet run --project eng/verify-samples -- --category 02-agents --log results.log
|
||||
|
||||
# Run specific samples by name
|
||||
dotnet run --project eng/verify-samples -- Agent_Step02_StructuredOutput Agent_Step09_AsFunctionTool
|
||||
|
||||
# Control parallelism (default 8)
|
||||
dotnet run --project eng/verify-samples -- --parallel 8 --log results.log
|
||||
|
||||
# Build samples during run (skips the need for a prior build step)
|
||||
# This may cause build conflicts as multiple samples are built in parallel, so use with caution
|
||||
dotnet run --project eng/verify-samples -- --build --log results.log
|
||||
|
||||
# Combine options
|
||||
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv --md results.md
|
||||
```
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
The tool itself needs:
|
||||
- `AZURE_OPENAI_ENDPOINT` — for the AI verification agent
|
||||
- `AZURE_OPENAI_DEPLOYMENT_NAME` (optional, defaults to `gpt-5-mini`)
|
||||
|
||||
Individual samples require their own env vars (e.g., `AZURE_AI_PROJECT_ENDPOINT`). The tool automatically checks and skips samples with missing env vars.
|
||||
|
||||
### Output Files
|
||||
|
||||
- `--log results.log` — detailed per-sample log with stdout/stderr, AI reasoning, and a summary
|
||||
- `--csv results.csv` — tabular summary with Sample, ProjectPath, Status, FailedChecks, and Failures columns
|
||||
- `--md results.md` — Markdown summary with results table and collapsible failure details (suitable for GitHub PR comments)
|
||||
|
||||
## Sample Categories
|
||||
|
||||
Definitions are in the `dotnet/eng/verify-samples/` directory:
|
||||
|
||||
| Category | Config File | Registered Key |
|
||||
|----------|-------------|----------------|
|
||||
| 01-get-started | `GetStartedSamples.cs` | `01-get-started` |
|
||||
| 02-agents | `AgentsSamples.cs` | `02-agents` |
|
||||
| 03-workflows | `WorkflowSamples.cs` | `03-workflows` |
|
||||
|
||||
Categories are registered in `VerifyOptions.cs` in the `s_sampleSets` dictionary.
|
||||
|
||||
## SampleDefinition Properties
|
||||
|
||||
Each sample is defined as a `SampleDefinition` in the appropriate config file. Key properties:
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
// Required: Display name for the sample
|
||||
Name = "Agent_Step02_StructuredOutput",
|
||||
|
||||
// Required: Relative path from dotnet/ to the sample project directory
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step02_StructuredOutput",
|
||||
|
||||
// Environment variables the sample requires (throws if missing)
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
|
||||
// Environment variables with defaults that would prompt on console if unset
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
|
||||
// Skip this sample with a reason (for structural issues only)
|
||||
SkipReason = null, // or "Requires external service X."
|
||||
|
||||
// Deterministic checks: substrings that must appear in stdout
|
||||
MustContain = ["=== Section Header ==="],
|
||||
|
||||
// Substrings that must NOT appear in stdout
|
||||
MustNotContain = [],
|
||||
|
||||
// If true, only MustContain checks are used (no AI verification)
|
||||
IsDeterministic = false,
|
||||
|
||||
// AI verification: natural-language descriptions of expected output
|
||||
// Each entry describes one aspect to verify independently
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show structured person information with Name, Age, and Occupation fields.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
|
||||
// Stdin inputs to feed to the sample (for interactive samples)
|
||||
Inputs = ["Y", "Y", "Y"],
|
||||
|
||||
// Delay between stdin inputs in ms (default 2000, increase for LLM calls between inputs)
|
||||
InputDelayMs = 3000,
|
||||
}
|
||||
```
|
||||
|
||||
## How to Add a New Sample Definition
|
||||
|
||||
1. **Check the sample's Program.cs** to understand:
|
||||
- What environment variables it reads (look for `GetEnvironmentVariable`)
|
||||
- Whether it needs stdin input (look for `Console.ReadLine`, `Application.GetInput`)
|
||||
- Whether it has an external loop (look for `EXIT` patterns in YAML workflows)
|
||||
- What output it produces (section headers, markers, expected behavior)
|
||||
- Whether it exits on its own or runs as a server
|
||||
|
||||
2. **Choose the right verification strategy:**
|
||||
- **Deterministic** (`IsDeterministic = true`): Use `MustContain` for samples with fixed output strings. No AI verification.
|
||||
- **AI-verified** (default): Use `ExpectedOutputDescription` with semantic descriptions. Write expectations that are flexible enough for non-deterministic LLM output.
|
||||
- **Both**: Use `MustContain` for fixed markers AND `ExpectedOutputDescription` for LLM-generated content.
|
||||
|
||||
3. **Set `SkipReason` only for structural issues:**
|
||||
- Web servers that don't exit
|
||||
- Multi-process client/server architectures
|
||||
- Samples requiring external infrastructure (MCP servers you can't reach, Docker, etc.)
|
||||
- Do NOT skip for missing env vars — the tool checks those dynamically.
|
||||
|
||||
4. **For interactive samples, provide `Inputs`:**
|
||||
- Samples using `Application.GetInput(args)` need one initial input
|
||||
- Samples with `Console.ReadLine()` approval loops need `"Y"` inputs
|
||||
- YAML workflows with `externalLoop` need `"EXIT"` as the last input
|
||||
- Set `InputDelayMs` to 3000-8000ms for samples with LLM calls between inputs
|
||||
|
||||
5. **Add the definition** to the appropriate config file (e.g., `AgentsSamples.cs`) in the `All` list.
|
||||
|
||||
6. **Register new categories** (if needed) in `VerifyOptions.cs` `s_sampleSets` dictionary.
|
||||
|
||||
### Writing Good ExpectedOutputDescription
|
||||
|
||||
- Write descriptions that are **semantically flexible** — LLM output varies between runs
|
||||
- Each array entry should describe **one independent aspect** to verify
|
||||
- Always include `"The output should not contain error messages or stack traces."` as the last entry
|
||||
- Avoid exact wording expectations — use "should mention", "should contain information about", "should show"
|
||||
- Bad: `"The output should say 'The weather in Amsterdam is cloudy with a high of 15°C'"`
|
||||
- Good: `"The output should contain weather information about Amsterdam mentioning cloudy weather with a high of 15°C."`
|
||||
|
||||
### Example: Simple LLM Sample
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_With_AzureOpenAIChatCompletion",
|
||||
ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a joke about a pirate.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Deterministic Sample
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_GenerateCode",
|
||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
||||
IsDeterministic = true,
|
||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Interactive Sample with Approval Loop
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "FoundryAgent_Hosted_MCP",
|
||||
ProjectPath = "samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["Y", "Y", "Y", "Y", "Y"],
|
||||
InputDelayMs = 5000,
|
||||
ExpectedOutputDescription = ["The output should show an agent using the Microsoft Learn MCP tool with approval prompts."],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Declarative Workflow with External Loop
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_FunctionTools",
|
||||
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What are today's specials?", "EXIT"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow calling function tools to answer a question about restaurant specials."],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Skipped Sample
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_MCP_Server",
|
||||
ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_Server",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
|
||||
},
|
||||
```
|
||||
+1
-8
@@ -402,11 +402,4 @@ FodyWeavers.xsd
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
# Foundry agent CLI config (contains secrets, auto-generated)
|
||||
.foundry-agent.json
|
||||
.foundry-agent-build.log
|
||||
|
||||
# Pre-published output for Docker builds
|
||||
out/
|
||||
*.sln.iml
|
||||
Vendored
+1
-2
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "agent-framework-dotnet.slnx",
|
||||
"git.openRepositoryInParentFolders": "always",
|
||||
"chat.agent.enabled": true,
|
||||
"dotnet.automaticallySyncWithActiveItem": true
|
||||
"chat.agent.enabled": true
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
Instructions for AI coding agents working in the .NET codebase.
|
||||
|
||||
## Build, Test, and Lint Commands
|
||||
|
||||
See `./.github/skills/build-and-test/SKILL.md` for detailed instructions on building, testing, and linting projects.
|
||||
|
||||
## Project Structure
|
||||
|
||||
See `./.github/skills/project-structure/SKILL.md` for an overview of the project structure.
|
||||
|
||||
### Core types
|
||||
|
||||
- `AIAgent`: The abstract base class that all agents derive from, providing common methods for interacting with an agent.
|
||||
- `AgentSession`: The abstract base class that all agent sessions derive from, representing a conversation with an agent.
|
||||
- `ChatClientAgent`: An `AIAgent` implementation that uses an `IChatClient` to send messages to an AI provider and receive responses.
|
||||
- `IChatClient`: Interface for sending messages to an AI provider and receiving responses. Used by `ChatClientAgent` and implemented by provider-specific packages.
|
||||
- `FunctionInvokingChatClient`: Decorator for `IChatClient` that adds function invocation capabilities.
|
||||
- `AITool`: Represents a tool that an agent/AI provider can use, with metadata and an execution delegate.
|
||||
- `AIFunction`: A specific type of `AITool` that represents a local function the agent/AI provider can call, with parameters and return types defined.
|
||||
- `ChatMessage`: Represents a message in a conversation.
|
||||
- `AIContent`: Represents content in a message, which can be text, a function call, tool output and more.
|
||||
|
||||
### External Dependencies
|
||||
|
||||
The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Abstractions` (external NuGet packages)
|
||||
using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, `AIFunction`, `ChatMessage`, and `AIContent`.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- **Command output capture**: When running `dotnet build`, `dotnet test`, `dotnet format`, or similar commands, redirect output to a temp file first (e.g., `dotnet build --tl:off 2>&1 | Out-File $env:TEMP\build.log`), then analyze the file as needed. This avoids re-running expensive commands when the initial analysis misses something.
|
||||
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly. When using PowerShell `Set-Content`, always pass `-Encoding UTF8BOM` to preserve the BOM (e.g., `Set-Content $file $content -NoNewline -Encoding UTF8BOM`).
|
||||
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
|
||||
- **XML docs**: Required for all public methods and classes
|
||||
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
|
||||
- **Private classes**: Should be `sealed` unless subclassed
|
||||
- **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming
|
||||
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking; test methods returning `Task`/`ValueTask` must use the `Async` suffix.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
When developing or reviewing code, verify adherence to these key design principles:
|
||||
|
||||
- **DRY**: Avoid code duplication by moving common logic into helper methods or helper classes.
|
||||
- **Single Responsibility**: Each class should have one clear responsibility.
|
||||
- **Encapsulation**: Keep implementation details private and expose only necessary public APIs.
|
||||
- **Strong Typing**: Use strong typing to ensure that code is self-documenting and to catch errors at compile time.
|
||||
|
||||
## Sample Structure
|
||||
|
||||
Samples (in `./samples/` folder) should follow this structure:
|
||||
|
||||
1. Copyright header: `// Copyright (c) Microsoft. All rights reserved.`
|
||||
2. Description comment explaining what the sample demonstrates
|
||||
3. Using statements
|
||||
4. Main code logic
|
||||
5. Helper methods at bottom
|
||||
|
||||
Configuration via environment variables (never hardcode secrets). Keep samples simple and focused.
|
||||
|
||||
When adding a new sample:
|
||||
|
||||
- Create a standalone project in `samples/` with matching directory and project names
|
||||
- Include a README.md explaining what the sample does and how to run it
|
||||
- Add the project to the solution file
|
||||
- Reference the sample in the parent directory's README.md
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<IsReleaseCandidate>false</IsReleaseCandidate>
|
||||
<IsReleased>false</IsReleased>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
|
||||
<Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.1.3" />
|
||||
<!-- Only run 'dotnet format' on dev machines, Release builds. Skip on GitHub Actions -->
|
||||
<!-- as this runs in its own Actions job. Only run for net10.0 target frameworks since the dotnet format command -->
|
||||
<!-- already formats all target frameworks in project. Otherwise it will run format x times x where x is the number of target frameworks -->
|
||||
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' AND '$(TargetFramework)' == 'net10.0' ">
|
||||
<!-- as this runs in its own Actions job. -->
|
||||
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' ">
|
||||
<Message Text="Running dotnet format" Importance="high" />
|
||||
<Exec Command="dotnet format --no-restore -v diag $(ProjectFileName)" />
|
||||
</Target>
|
||||
|
||||
@@ -7,119 +7,110 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Aspire -->
|
||||
<AspireAppHostSdkVersion>13.1.0</AspireAppHostSdkVersion>
|
||||
<AspireAppHostSdkVersion>13.0.2</AspireAppHostSdkVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.13.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.5.0" />
|
||||
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Anthropic" Version="12.0.1" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.Inference" Version="13.1.0-preview.1.25616.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.AIFoundry" Version="13.1.0-preview.1.25616.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.23" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.3" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.4" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Core" Version="1.53.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
|
||||
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<!-- Google Gemini -->
|
||||
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
|
||||
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
|
||||
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
||||
<!-- Microsoft.Azure.* -->
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
|
||||
<!-- Newtonsoft.Json -->
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.10.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
|
||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.6" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.6" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.6" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.5.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.5.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.4.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.4.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.3.0-preview.1.26109.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.2.0-preview.1.26063.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Compliance.Abstractions" Version="10.5.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
|
||||
<!-- Vector Stores -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
|
||||
<!-- Semantic Kernel -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.29" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
<PackageVersion Include="Microsoft.Agents.Authentication.Msal" Version="1.3.171-beta" />
|
||||
<PackageVersion Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.3.171-beta" />
|
||||
<!-- A2A -->
|
||||
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
|
||||
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
|
||||
<PackageVersion Include="A2A" Version="0.3.3-preview" />
|
||||
<PackageVersion Include="A2A.AspNetCore" Version="0.3.3-preview" />
|
||||
<!-- MCP -->
|
||||
<PackageVersion Include="ModelContextProtocol" Version="1.1.0" />
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
|
||||
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.10.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.8.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
|
||||
<!-- Workflows -->
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.4.1" />
|
||||
<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" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
|
||||
@@ -127,7 +118,8 @@
|
||||
<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.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
@@ -142,15 +134,15 @@
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net10.0'" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="xRetry.v3" Version="1.0.0-rc3" />
|
||||
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.4.1" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xretry" Version="1.9.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<!-- Symbols -->
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<!-- Toolset -->
|
||||
<PackageVersion Include="ReferenceTrimmer" Version="3.4.5" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
|
||||
+11
-5
@@ -1,20 +1,26 @@
|
||||
# Get Started with Microsoft Agent Framework for C# Developers
|
||||
|
||||
## Samples
|
||||
|
||||
- [Getting Started with Agents](./samples/GettingStarted/Agents): basic agent creation and tool usage
|
||||
- [Agent Provider Samples](./samples/GettingStarted/AgentProviders): samples showing different agent providers
|
||||
- [Workflow Samples](./samples/GettingStarted/Workflows): advanced multi-agent patterns and workflow orchestration
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Basic Agent - .NET
|
||||
|
||||
```c#
|
||||
using System;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!;
|
||||
|
||||
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
.GetResponsesClient(deploymentName)
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
@@ -22,9 +28,9 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
||||
|
||||
## Examples & Samples
|
||||
|
||||
- [Getting Started with Agents](./samples/02-agents/Agents): basic agent creation and tool usage
|
||||
- [Agent Provider Samples](./samples/02-agents/AgentProviders): samples showing different agent providers
|
||||
- [Workflow Samples](./samples/03-workflows): advanced multi-agent patterns and workflow orchestration
|
||||
- [Getting Started with Agents](./samples/GettingStarted/Agents): basic agent creation and tool usage
|
||||
- [Agent Provider Samples](./samples/GettingStarted/AgentProviders): samples showing different agent providers
|
||||
- [Workflow Samples](./samples/GettingStarted/Workflows): advanced multi-agent patterns and workflow orchestration
|
||||
|
||||
## Agent Framework Documentation
|
||||
|
||||
|
||||
+221
-377
@@ -5,393 +5,274 @@
|
||||
<BuildType Name="Release" />
|
||||
</Configurations>
|
||||
<Folder Name="/Samples/">
|
||||
<File Path="samples/AGENTS.md" />
|
||||
<File Path="samples/README.md" />
|
||||
<Project Path="eng/verify-samples/verify-samples.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/01-get-started/">
|
||||
<Project Path="samples/01-get-started/01_hello_agent/01_hello_agent.csproj" />
|
||||
<Project Path="samples/01-get-started/02_add_tools/02_add_tools.csproj" />
|
||||
<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 Name="/Samples/A2AClientServer/">
|
||||
<File Path="samples/A2AClientServer/README.md" />
|
||||
<Project Path="samples/A2AClientServer/A2AClient/A2AClient.csproj" />
|
||||
<Project Path="samples/A2AClientServer/A2AServer/A2AServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/">
|
||||
<File Path="samples/02-agents/README.md" />
|
||||
<Folder Name="/Samples/AgentWebChat/">
|
||||
<Project Path="samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj" />
|
||||
<Project Path="samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj" />
|
||||
<Project Path="samples/AgentWebChat/AgentWebChat.ServiceDefaults/AgentWebChat.ServiceDefaults.csproj" />
|
||||
<Project Path="samples/AgentWebChat/AgentWebChat.Web/AgentWebChat.Web.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentProviders/">
|
||||
<File Path="samples/02-agents/AgentProviders/README.md" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_GitHubCopilot/Agent_With_GitHubCopilot.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
|
||||
<Folder Name="/Samples/AGUIClientServer/">
|
||||
<Project Path="samples/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/DevUIAspireIntegration/">
|
||||
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.AppHost/DevUIIntegration.AppHost.csproj" />
|
||||
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.ServiceDefaults/DevUIIntegration.ServiceDefaults.csproj" />
|
||||
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/EditorAgent/EditorAgent.csproj" />
|
||||
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/WriterAgent/WriterAgent.csproj" />
|
||||
<Folder Name="/Samples/AzureFunctions/">
|
||||
<File Path="samples/AzureFunctions/.editorconfig" />
|
||||
<File Path="samples/AzureFunctions/README.md" />
|
||||
<Project Path="samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/Agents/">
|
||||
<File Path="samples/02-agents/Agents/README.md" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step01_UsingFunctionToolsWithApprovals/Agent_Step01_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step02_StructuredOutput/Agent_Step02_StructuredOutput.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step03_PersistedConversations/Agent_Step03_PersistedConversations.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step04_3rdPartyChatHistoryStorage/Agent_Step04_3rdPartyChatHistoryStorage.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step05_Observability/Agent_Step05_Observability.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step06_DependencyInjection/Agent_Step06_DependencyInjection.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step07_AsMcpTool/Agent_Step07_AsMcpTool.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step08_UsingImages/Agent_Step08_UsingImages.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step09_AsFunctionTool/Agent_Step09_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step10_BackgroundResponsesWithToolsAndPersistence/Agent_Step10_BackgroundResponsesWithToolsAndPersistence.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step11_Middleware/Agent_Step11_Middleware.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step12_Plugins/Agent_Step12_Plugins.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step13_ChatReduction/Agent_Step13_ChatReduction.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step14_BackgroundResponses/Agent_Step14_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step15_DeepResearch/Agent_Step15_DeepResearch.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Agent_Step17_AdditionalAIContext.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step20_DynamicFunctionTools/Agent_Step20_DynamicFunctionTools.csproj" />
|
||||
<Folder Name="/Samples/DurableAgents/">
|
||||
<File Path="samples/DurableAgents/ConsoleApps/README.md" />
|
||||
</Folder>
|
||||
<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 Name="/Samples/DurableAgents/ConsoleApps/">
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/">
|
||||
<File Path="samples/02-agents/AGUI/README.md" />
|
||||
<Folder Name="/Samples/GettingStarted/A2A/">
|
||||
<File Path="samples/GettingStarted/A2A/README.md" />
|
||||
<Project Path="samples/GettingStarted/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step01_GettingStarted/">
|
||||
<Project Path="samples/02-agents/AGUI/Step01_GettingStarted/Client/Client.csproj" />
|
||||
<Project Path="samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AgentProviders/">
|
||||
<File Path="samples/GettingStarted/AgentProviders/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_OpenAIAssistants/Agent_With_OpenAIAssistants.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step02_BackendTools/">
|
||||
<Project Path="samples/02-agents/AGUI/Step02_BackendTools/Client/Client.csproj" />
|
||||
<Project Path="samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Agents/">
|
||||
<File Path="samples/GettingStarted/Agents/README.md" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step08_Observability/Agent_Step08_Observability.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Agent_Step13_BackgroundResponsesWithToolsAndPersistence.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step14_Middleware/Agent_Step14_Middleware.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step15_Plugins/Agent_Step15_Plugins.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Agent_Step20_AdditionalAIContext.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step03_FrontendTools/">
|
||||
<Project Path="samples/02-agents/AGUI/Step03_FrontendTools/Client/Client.csproj" />
|
||||
<Project Path="samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
|
||||
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step04_HumanInLoop/">
|
||||
<Project Path="samples/02-agents/AGUI/Step04_HumanInLoop/Client/Client.csproj" />
|
||||
<Project Path="samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AGUI/">
|
||||
<File Path="samples/GettingStarted/AGUI/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentSkills/">
|
||||
<File Path="samples/02-agents/AgentSkills/README.md" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AGUI/Step01_GettingStarted/">
|
||||
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Client/Client.csproj" />
|
||||
<Project Path="samples/GettingStarted/AGUI/Step01_GettingStarted/Server/Server.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AGUI/Step02_BackendTools/">
|
||||
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Client/Client.csproj" />
|
||||
<Project Path="samples/GettingStarted/AGUI/Step02_BackendTools/Server/Server.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/DevUI/">
|
||||
<File Path="samples/02-agents/DevUI/README.md" />
|
||||
<Project Path="samples/02-agents/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AGUI/Step03_FrontendTools/">
|
||||
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Client/Client.csproj" />
|
||||
<Project Path="samples/GettingStarted/AGUI/Step03_FrontendTools/Server/Server.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentWithAnthropic/">
|
||||
<File Path="samples/02-agents/AgentWithAnthropic/README.md" />
|
||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AGUI/Step04_HumanInLoop/">
|
||||
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Client/Client.csproj" />
|
||||
<Project Path="samples/GettingStarted/AGUI/Step04_HumanInLoop/Server/Server.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentsWithFoundry/">
|
||||
<File Path="samples/02-agents/AgentsWithFoundry/README.md" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Agent_Step25_ToolboxServerSideTools.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AGUI/Step05_StateManagement/">
|
||||
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||
<Project Path="samples/GettingStarted/AGUI/Step05_StateManagement/Server/Server.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/Evaluation/">
|
||||
<Project Path="samples/02-agents/Evaluation/Evaluation_SimpleEval/Evaluation_SimpleEval.csproj" />
|
||||
<Project Path="samples/02-agents/Evaluation/Evaluation_CustomEvals/Evaluation_CustomEvals.csproj" />
|
||||
<Project Path="samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/Evaluation_ExpectedOutputs.csproj" />
|
||||
<Project Path="samples/02-agents/Evaluation/Evaluation_Multimodal/Evaluation_Multimodal.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/DevUI/">
|
||||
<File Path="samples/GettingStarted/DevUI/README.md" />
|
||||
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentWithMemory/">
|
||||
<File Path="samples/02-agents/AgentWithMemory/README.md" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
|
||||
<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" />
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithAnthropic/">
|
||||
<File Path="samples/GettingStarted/AgentWithAnthropic/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentWithOpenAI/">
|
||||
<File Path="samples/02-agents/AgentWithOpenAI/README.md" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
|
||||
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/AgentWithMemory_Step03_CustomMemory.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentWithRAG/">
|
||||
<File Path="samples/02-agents/AgentWithRAG/README.md" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
|
||||
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
||||
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj" />
|
||||
<Folder Name="/Samples/Purview/" />
|
||||
<Folder Name="/Samples/Purview/AgentWithPurview/">
|
||||
<Project Path="samples/Purview/AgentWithPurview/AgentWithPurview.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/Observability/">
|
||||
<Project Path="samples/02-agents/AgentOpenTelemetry/AgentOpenTelemetry.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithRAG/">
|
||||
<File Path="samples/GettingStarted/AgentWithRAG/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/">
|
||||
<File Path="samples/03-workflows/README.md" />
|
||||
<Folder Name="/Samples/GettingStarted/FoundryAgents/">
|
||||
<File Path="samples/GettingStarted/FoundryAgents/README.md" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/FoundryAgents_Step01.1_Basics.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/FoundryAgents_Step01.2_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/FoundryAgents_Step02_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/FoundryAgents_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/FoundryAgents_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step06_PersistedConversations/FoundryAgents_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step07_Observability/FoundryAgents_Step07_Observability.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step08_DependencyInjection/FoundryAgents_Step08_DependencyInjection.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step09_UsingMcpClientAsTools/FoundryAgents_Step09_UsingMcpClientAsTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/FoundryAgents_Step11_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step12_Middleware/FoundryAgents_Step12_Middleware.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step13_Plugins/FoundryAgents_Step13_Plugins.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/FoundryAgents_Step14_CodeInterpreter.csproj" />
|
||||
<Project Path="samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/FoundryAgents_Step15_ComputerUse.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Concurrent/">
|
||||
<Project Path="samples/03-workflows/Concurrent/Concurrent/Concurrent.csproj" />
|
||||
<Project Path="samples/03-workflows/Concurrent/MapReduce/MapReduce.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/ModelContextProtocol/">
|
||||
<File Path="samples/GettingStarted/ModelContextProtocol/README.md" />
|
||||
<Project Path="samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
|
||||
<Project Path="samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
|
||||
<Project Path="samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
|
||||
<Project Path="samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/ConditionalEdges/">
|
||||
<Project Path="samples/03-workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj" />
|
||||
<Project Path="samples/03-workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj" />
|
||||
<Project Path="samples/03-workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Observability/">
|
||||
<Project Path="samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Declarative/">
|
||||
<File Path="samples/03-workflows/Declarative/README.md" />
|
||||
<Project Path="samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/StudentTeacher/StudentTeacher.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/">
|
||||
<File Path="samples/GettingStarted/Workflows/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Declarative/Examples/">
|
||||
<File Path="../declarative-agents/workflow-samples/CustomerSupport.yaml" />
|
||||
<File Path="../declarative-agents/workflow-samples/DeepResearch.yaml" />
|
||||
<File Path="../declarative-agents/workflow-samples/Marketing.yaml" />
|
||||
<File Path="../declarative-agents/workflow-samples/MathChat.yaml" />
|
||||
<File Path="../declarative-agents/workflow-samples/README.md" />
|
||||
<File Path="../declarative-agents/workflow-samples/wttr.json" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Concurrent/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Concurrent/Concurrent/Concurrent.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Concurrent/MapReduce/MapReduce.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/SharedStates/">
|
||||
<Project Path="samples/03-workflows/SharedStates/SharedStates.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/ConditionalEdges/">
|
||||
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Loop/">
|
||||
<Project Path="samples/03-workflows/Loop/Loop.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/">
|
||||
<File Path="samples/GettingStarted/Workflows/Declarative/README.md" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/DeepResearch/DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Agents/">
|
||||
<Project Path="samples/03-workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
|
||||
<Project Path="samples/03-workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
|
||||
<Project Path="samples/03-workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj" />
|
||||
<Project Path="samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
|
||||
<File Path="../workflow-samples/CustomerSupport.yaml" />
|
||||
<File Path="../workflow-samples/DeepResearch.yaml" />
|
||||
<File Path="../workflow-samples/Marketing.yaml" />
|
||||
<File Path="../workflow-samples/MathChat.yaml" />
|
||||
<File Path="../workflow-samples/README.md" />
|
||||
<File Path="../workflow-samples/wttr.json" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Checkpoint/">
|
||||
<Project Path="samples/03-workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj" />
|
||||
<Project Path="samples/03-workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/SharedStates/">
|
||||
<Project Path="samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/HumanInTheLoop/">
|
||||
<Project Path="samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Loop/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Loop/Loop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Orchestration/">
|
||||
<Project Path="samples/03-workflows/Orchestration/Handoff/Handoff.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Agents/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Observability/">
|
||||
<Project Path="samples/03-workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
|
||||
<Project Path="samples/03-workflows/Observability/AspireDashboard/AspireDashboard.csproj" />
|
||||
<Project Path="samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Visualization/">
|
||||
<Project Path="samples/03-workflows/Visualization/Visualization.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/HumanInTheLoop/">
|
||||
<Project Path="samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/_StartHere/">
|
||||
<Project Path="samples/03-workflows/_StartHere/01_Streaming/01_Streaming.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/02_AgentsInWorkflows/02_AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/03_AgentWorkflowPatterns/03_AgentWorkflowPatterns.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/04_MultiModelService/04_MultiModelService.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/05_SubWorkflows/05_SubWorkflows.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/06_MixedWorkflowAgentsAndExecutors.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/07_WriterCriticWorkflow/07_WriterCriticWorkflow.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Observability/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Observability/AspireDashboard/AspireDashboard.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Evaluation/">
|
||||
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Evaluation_WorkflowEval.csproj" />
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Visualization/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Visualization/Visualization.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/">
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/_Foundational/">
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/04_AgentWorkflowPatterns.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/06_SubWorkflows.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/" />
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/" />
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj" />
|
||||
<Folder Name="/Samples/HostedAgents/">
|
||||
<Project Path="samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/" />
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.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" />
|
||||
<Project Path="samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
|
||||
<Project Path="samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj" />
|
||||
<Project Path="samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/">
|
||||
<Project Path="samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj" />
|
||||
<Project Path="samples/05-end-to-end/M365Agent/M365Agent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/Evaluation/">
|
||||
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Evaluation_FoundryQuality.csproj" />
|
||||
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Evaluation_MixedProviders.csproj" />
|
||||
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Evaluation_ConversationSplits.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/A2AClientServer/">
|
||||
<File Path="samples/05-end-to-end/A2AClientServer/README.md" />
|
||||
<Project Path="samples/05-end-to-end/A2AClientServer/A2AClient/A2AClient.csproj" />
|
||||
<Project Path="samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/AgentWebChat/">
|
||||
<Project Path="samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AgentWebChat/AgentWebChat.ServiceDefaults/AgentWebChat.ServiceDefaults.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/AgentWebChat.Web.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/AGUIClientServer/">
|
||||
<File Path="samples/05-end-to-end/AGUIClientServer/README.md" />
|
||||
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
|
||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml" />
|
||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
|
||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
|
||||
<Folder Name="/Samples/M365Agent/">
|
||||
<Project Path="samples/M365Agent/M365Agent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
<File Path=".gitignore" />
|
||||
<File Path="AGENTS.md" />
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Build.targets" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="global.json" />
|
||||
<File Path="nuget.config" />
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/.github/" />
|
||||
<Folder Name="/Solution Items/.github/upgrades/" />
|
||||
@@ -400,6 +281,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/.github/workflows/">
|
||||
<File Path="../.github/workflows/dotnet-build-and-test.yml" />
|
||||
<File Path="../.github/workflows/dotnet-check-coverage.ps1" />
|
||||
<File Path="../.github/workflows/dotnet-format.yml" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/demos/">
|
||||
@@ -417,15 +299,6 @@
|
||||
<File Path="../docs/decisions/0007-agent-filtering-middleware.md" />
|
||||
<File Path="../docs/decisions/0008-python-subpackages.md" />
|
||||
<File Path="../docs/decisions/0009-support-long-running-operations.md" />
|
||||
<File Path="../docs/decisions/0010-ag-ui-support.md" />
|
||||
<File Path="../docs/decisions/0011-create-get-agent-api.md" />
|
||||
<File Path="../docs/decisions/0012-python-typeddict-options.md" />
|
||||
<File Path="../docs/decisions/0013-python-get-response-simplification.md" />
|
||||
<File Path="../docs/decisions/0014-feature-collections.md" />
|
||||
<File Path="../docs/decisions/0015-agent-run-context.md" />
|
||||
<File Path="../docs/decisions/0016-python-context-middleware.md" />
|
||||
<File Path="../docs/decisions/0017-agent-additional-properties.md" />
|
||||
<File Path="../docs/decisions/0018-agentthread-serialization.md" />
|
||||
<File Path="../docs/decisions/adr-short-template.md" />
|
||||
<File Path="../docs/decisions/adr-template.md" />
|
||||
<File Path="../docs/decisions/README.md" />
|
||||
@@ -436,10 +309,6 @@
|
||||
<File Path="eng/MSBuild/Shared.props" />
|
||||
<File Path="eng/MSBuild/Shared.targets" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/eng/scripts/">
|
||||
<File Path="eng/scripts/dotnet-check-coverage.ps1" />
|
||||
<File Path="eng/scripts/New-FilteredSolution.ps1" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/nuget/">
|
||||
<File Path="nuget/icon.png" />
|
||||
<File Path="nuget/nuget-package.props" />
|
||||
@@ -494,10 +363,6 @@
|
||||
<File Path="src/Shared/Demos/README.md" />
|
||||
<File Path="src/Shared/Demos/SampleEnvironment.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/DiagnosticIds/">
|
||||
<File Path="src/Shared/DiagnosticIds/DiagnosticsIds.cs" />
|
||||
<File Path="src/Shared/DiagnosticIds/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/IntegrationTests/">
|
||||
<File Path="src/Shared/IntegrationTests/AnthropicConfiguration.cs" />
|
||||
<File Path="src/Shared/IntegrationTests/AzureAIConfiguration.cs" />
|
||||
@@ -505,10 +370,6 @@
|
||||
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
|
||||
<File Path="src/Shared/IntegrationTests/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/IntegrationTestsAzureCredentials/">
|
||||
<File Path="src/Shared/IntegrationTestsAzureCredentials/README.md" />
|
||||
<File Path="src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/Samples/">
|
||||
<File Path="src/Shared/Samples/BaseSample.cs" />
|
||||
<File Path="src/Shared/Samples/README.md" />
|
||||
@@ -516,36 +377,26 @@
|
||||
<File Path="src/Shared/Samples/TextOutputHelperExtensions.cs" />
|
||||
<File Path="src/Shared/Samples/XunitLogger.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/Redaction/">
|
||||
<File Path="src/Shared/Redaction/README.md" />
|
||||
<File Path="src/Shared/Redaction/ReplacingRedactor.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/Throw/">
|
||||
<File Path="src/Shared/Throw/README.md" />
|
||||
<File Path="src/Shared/Throw/Throw.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/StructuredOutput/">
|
||||
<File Path="src/Shared/StructuredOutput/StructuredOutputSchemaUtilities.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/tests/">
|
||||
<File Path="tests/.editorconfig" />
|
||||
<File Path="tests/Directory.Build.props" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<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/Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<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" />
|
||||
@@ -555,10 +406,8 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
</Folder>
|
||||
@@ -566,11 +415,10 @@
|
||||
<Folder Name="/Tests/IntegrationTests/">
|
||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.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.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
|
||||
@@ -580,18 +428,16 @@
|
||||
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Tests/UnitTests/">
|
||||
<Project Path="tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||
<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.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.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" />
|
||||
@@ -601,9 +447,7 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
</Solution>
|
||||
@@ -6,16 +6,13 @@
|
||||
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
|
||||
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
|
||||
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
|
||||
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
|
||||
"src\\Microsoft.Agents.AI.Foundry\\Microsoft.Agents.AI.Foundry.csproj",
|
||||
"src\\Microsoft.Agents.AI.Foundry.Hosting\\Microsoft.Agents.AI.Foundry.Hosting.csproj",
|
||||
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
|
||||
"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",
|
||||
@@ -25,12 +22,10 @@
|
||||
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
||||
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
|
||||
"src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj",
|
||||
"src\\Aspire.Hosting.AgentFramework.DevUI\\Aspire.Hosting.AgentFramework.DevUI.csproj"
|
||||
"src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTests\*.cs" LinkBase="Shared\IntegrationTests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
||||
</ItemGroup>
|
||||
@@ -23,13 +20,4 @@
|
||||
<ItemGroup Condition="'$(InjectSharedFoundryAgents)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Foundry\Agents\*.cs" LinkBase="Shared\Foundry" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedStructuredOutput)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StructuredOutput\*.cs" LinkBase="Shared\StructuredOutput" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedDiagnosticIds)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\DiagnosticIds\*.cs" LinkBase="Shared\DiagnosticIds" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generates a filtered .slnx solution file by removing projects that don't match the specified criteria.
|
||||
|
||||
.DESCRIPTION
|
||||
Parses a .slnx solution file and applies one or more filters:
|
||||
- Removes projects that don't support the specified target framework (via MSBuild query).
|
||||
- Optionally removes all sample projects (under samples/).
|
||||
- Optionally filters test projects by name pattern (e.g., only *UnitTests*).
|
||||
Writes the filtered solution to the specified output path and prints the path.
|
||||
|
||||
.PARAMETER Solution
|
||||
Path to the source .slnx solution file.
|
||||
|
||||
.PARAMETER TargetFramework
|
||||
The target framework to filter by (e.g., net10.0, net472).
|
||||
|
||||
.PARAMETER Configuration
|
||||
Optional MSBuild configuration used when querying TargetFrameworks. Defaults to Debug.
|
||||
|
||||
.PARAMETER TestProjectNameFilter
|
||||
Optional wildcard pattern to filter test project names (e.g., *UnitTests*, *IntegrationTests*).
|
||||
When specified, only test projects whose filename matches this pattern are kept.
|
||||
|
||||
.PARAMETER ExcludeSamples
|
||||
When specified, removes all projects under the samples/ directory from the solution.
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Optional output path for the filtered .slnx file. If not specified, a temp file is created.
|
||||
|
||||
.EXAMPLE
|
||||
# Generate a filtered solution and run tests
|
||||
$filtered = ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472
|
||||
dotnet test --solution $filtered --no-build -f net472
|
||||
|
||||
.EXAMPLE
|
||||
# Generate a solution with only unit test projects
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameFilter "*UnitTests*" -OutputPath filtered-unit.slnx
|
||||
|
||||
.EXAMPLE
|
||||
# Inline usage with dotnet test (PowerShell)
|
||||
dotnet test --solution (./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472) --no-build -f net472
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Solution,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$TargetFramework,
|
||||
|
||||
[string]$Configuration = "Debug",
|
||||
|
||||
[string]$TestProjectNameFilter,
|
||||
|
||||
[switch]$ExcludeSamples,
|
||||
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Resolve the solution path
|
||||
$solutionPath = Resolve-Path $Solution
|
||||
$solutionDir = Split-Path $solutionPath -Parent
|
||||
|
||||
if (-not $OutputPath) {
|
||||
$OutputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "filtered-$(Split-Path $solutionPath -Leaf)")
|
||||
}
|
||||
|
||||
# Parse the .slnx XML
|
||||
[xml]$slnx = Get-Content $solutionPath -Raw
|
||||
|
||||
$removed = @()
|
||||
$kept = @()
|
||||
|
||||
# Remove sample projects if requested
|
||||
if ($ExcludeSamples) {
|
||||
$sampleProjects = $slnx.SelectNodes("//Project[contains(@Path, 'samples/')]")
|
||||
foreach ($proj in $sampleProjects) {
|
||||
$projRelPath = $proj.GetAttribute("Path")
|
||||
Write-Verbose "Removing (sample): $projRelPath"
|
||||
$removed += $projRelPath
|
||||
$proj.ParentNode.RemoveChild($proj) | Out-Null
|
||||
}
|
||||
Write-Host "Removed $($sampleProjects.Count) sample project(s)." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Filter all remaining projects by target framework
|
||||
$allProjects = $slnx.SelectNodes("//Project")
|
||||
|
||||
foreach ($proj in $allProjects) {
|
||||
$projRelPath = $proj.GetAttribute("Path")
|
||||
$projFullPath = Join-Path $solutionDir $projRelPath
|
||||
$projFileName = Split-Path $projRelPath -Leaf
|
||||
$isTestProject = $projRelPath -like "*tests/*"
|
||||
|
||||
# Filter test projects by name pattern if specified
|
||||
if ($isTestProject -and $TestProjectNameFilter -and ($projFileName -notlike $TestProjectNameFilter)) {
|
||||
Write-Verbose "Removing (name filter): $projRelPath"
|
||||
$removed += $projRelPath
|
||||
$proj.ParentNode.RemoveChild($proj) | Out-Null
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Test-Path $projFullPath)) {
|
||||
Write-Verbose "Project not found, keeping in solution: $projRelPath"
|
||||
$kept += $projRelPath
|
||||
continue
|
||||
}
|
||||
|
||||
# Query the project's target frameworks using MSBuild
|
||||
$targetFrameworks = & dotnet msbuild $projFullPath -getProperty:TargetFrameworks -p:Configuration=$Configuration -nologo 2>$null
|
||||
$targetFrameworks = $targetFrameworks.Trim()
|
||||
|
||||
if ($targetFrameworks -like "*$TargetFramework*") {
|
||||
Write-Verbose "Keeping: $projRelPath (targets: $targetFrameworks)"
|
||||
$kept += $projRelPath
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Removing: $projRelPath (targets: $targetFrameworks, missing: $TargetFramework)"
|
||||
$removed += $projRelPath
|
||||
$proj.ParentNode.RemoveChild($proj) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Write the filtered solution
|
||||
$slnx.Save($OutputPath)
|
||||
|
||||
# Report results to stderr so stdout is clean for piping
|
||||
Write-Host "Filtered solution written to: $OutputPath" -ForegroundColor Green
|
||||
if ($removed.Count -gt 0) {
|
||||
Write-Host "Removed $($removed.Count) project(s):" -ForegroundColor Yellow
|
||||
foreach ($r in $removed) {
|
||||
Write-Host " - $r" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
Write-Host "Kept $($kept.Count) project(s)." -ForegroundColor Green
|
||||
|
||||
# Output the path for piping
|
||||
Write-Output $OutputPath
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe console output with sample-name prefixes and colored status.
|
||||
/// </summary>
|
||||
internal sealed class ConsoleReporter
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete prefixed line atomically to the console.
|
||||
/// </summary>
|
||||
public void WriteLineWithPrefix(string sampleName, string message, ConsoleColor? color = null)
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"[{sampleName}] ");
|
||||
if (color.HasValue)
|
||||
{
|
||||
Console.ForegroundColor = color.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the final summary table and elapsed time to the console.
|
||||
/// </summary>
|
||||
public void PrintSummary(
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
TimeSpan elapsed)
|
||||
{
|
||||
var passCount = orderedResults.Count(r => r.Passed);
|
||||
var failCount = orderedResults.Count(r => !r.Passed);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(new string('─', 60));
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine("SUMMARY");
|
||||
Console.ResetColor();
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
Console.ForegroundColor = result.Passed ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
Console.Write(result.Passed ? " ✓ " : " ✗ ");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine($"{result.SampleName}: {result.Summary}");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.Write(" ○ ");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine($"{name}: Skipped — {reason}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.Write("Results: ");
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.Write($"{passCount} passed");
|
||||
Console.ResetColor();
|
||||
|
||||
if (failCount > 0)
|
||||
{
|
||||
Console.Write(", ");
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Write($"{failCount} failed");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
if (skipped.Count > 0)
|
||||
{
|
||||
Console.Write(", ");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.Write($"{skipped.Count} skipped");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a CSV summary of sample verification results.
|
||||
/// </summary>
|
||||
internal static class CsvResultWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the results to a CSV file at the specified path.
|
||||
/// </summary>
|
||||
public static async Task WriteAsync(
|
||||
string path,
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
IReadOnlyList<SampleDefinition> samples)
|
||||
{
|
||||
var pathLookup = samples.ToDictionary(s => s.Name, s => s.ProjectPath);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Sample,ProjectPath,Status,FailedChecks,Failures");
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
var status = result.Passed ? "PASSED" : "FAILED";
|
||||
var failedChecks = result.Failures.Count;
|
||||
var failures = string.Join("; ", result.Failures);
|
||||
pathLookup.TryGetValue(result.SampleName, out var projectPath);
|
||||
sb.AppendLine($"{CsvEscape(result.SampleName)},{CsvEscape(projectPath ?? "")},{status},{failedChecks},{CsvEscape(failures)}");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
pathLookup.TryGetValue(name, out var projectPath);
|
||||
sb.AppendLine($"{CsvEscape(name)},{CsvEscape(projectPath ?? "")},SKIPPED,0,{CsvEscape(reason)}");
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a value for CSV: wraps in quotes if it contains commas, quotes, or newlines.
|
||||
/// </summary>
|
||||
private static string CsvEscape(string value)
|
||||
{
|
||||
if (value.Contains('"') || value.Contains(',') || value.Contains('\n') || value.Contains('\r'))
|
||||
{
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the expected behavior for each sample in 01-get-started.
|
||||
/// </summary>
|
||||
internal static class GetStartedSamples
|
||||
{
|
||||
public static IReadOnlyList<SampleDefinition> All { get; } =
|
||||
[
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "05_first_workflow",
|
||||
ProjectPath = "samples/01-get-started/05_first_workflow",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"UppercaseExecutor: HELLO, WORLD!",
|
||||
"ReverseTextExecutor: !DLROW ,OLLEH",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "01_hello_agent",
|
||||
ProjectPath = "samples/01-get-started/01_hello_agent",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a joke about a pirate.",
|
||||
"There should be two separate joke responses — one from a non-streaming call and one from a streaming call.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "02_add_tools",
|
||||
ProjectPath = "samples/01-get-started/02_add_tools",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain = [],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain information about the weather in Amsterdam.",
|
||||
"The response should mention that it is cloudy with a high of 15°C (or equivalent), since this comes from a tool that returns a canned response.",
|
||||
"There should be two responses — one from a non-streaming call and one from a streaming call.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "03_multi_turn",
|
||||
ProjectPath = "samples/01-get-started/03_multi_turn",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a joke about a pirate.",
|
||||
"After the initial joke, there should be a modified version that includes emojis and is told in the voice of a pirate's parrot.",
|
||||
"The pattern repeats: first a non-streaming pirate joke + parrot version, then a streaming pirate joke + parrot version.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "04_memory",
|
||||
ProjectPath = "samples/01-get-started/04_memory",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
">> Use session with blank memory",
|
||||
">> Use deserialized session with previously created memories",
|
||||
">> Read memories using memory component",
|
||||
"MEMORY - User Name:",
|
||||
"MEMORY - User Age:",
|
||||
">> Use new session with previously created memories",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"In the 'Use session with blank memory' section, the agent should respond to the user's messages. It may ask for the user's name or age if not yet known.",
|
||||
"In the 'Use deserialized session with previously created memories' section, the agent should correctly recall that the user's name is Ruaidhrí and age is 20.",
|
||||
"The 'MEMORY - User Name:' line should show 'Ruaidhrí' (or a close transliteration).",
|
||||
"The 'MEMORY - User Age:' line should show '20'.",
|
||||
"In the 'Use new session with previously created memories' section, the agent should know the user's name and age from the transferred memory.",
|
||||
"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 = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Incrementally writes a sequential (non-interleaved) log file, appending after each sample completes.
|
||||
/// Thread-safe: multiple parallel tasks may call write methods concurrently.
|
||||
/// </summary>
|
||||
internal sealed class LogFileWriter : IDisposable
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
public LogFileWriter(string path)
|
||||
{
|
||||
this._path = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._writeLock.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the log file header. Call once at the start of the run.
|
||||
/// </summary>
|
||||
public async Task WriteHeaderAsync()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Sample Verification Log — {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
||||
sb.AppendLine(new string('═', 72));
|
||||
sb.AppendLine();
|
||||
|
||||
await File.WriteAllTextAsync(this._path, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a skipped-sample entry to the log file.
|
||||
/// </summary>
|
||||
public async Task WriteSkippedAsync(string name, string reason)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"── {name} ──");
|
||||
sb.AppendLine($"Status: SKIPPED — {reason}");
|
||||
sb.AppendLine();
|
||||
|
||||
await this.AppendAsync(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a completed sample's full output section to the log file.
|
||||
/// </summary>
|
||||
public async Task WriteSampleResultAsync(VerificationResult result)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(new string('─', 72));
|
||||
sb.AppendLine($"── {result.SampleName} ──");
|
||||
sb.AppendLine($"Status: {(result.Passed ? "PASSED" : "FAILED")}");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var line in result.LogLines)
|
||||
{
|
||||
sb.AppendLine(line);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.Stdout))
|
||||
{
|
||||
sb.AppendLine("--- stdout ---");
|
||||
sb.AppendLine(result.Stdout.TrimEnd());
|
||||
sb.AppendLine("--- end stdout ---");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.Stderr))
|
||||
{
|
||||
sb.AppendLine("--- stderr ---");
|
||||
sb.AppendLine(result.Stderr.TrimEnd());
|
||||
sb.AppendLine("--- end stderr ---");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (result.Failures.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Failures:");
|
||||
foreach (var failure in result.Failures)
|
||||
{
|
||||
sb.AppendLine($" ✗ {failure}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (result.AIReasoning is not null)
|
||||
{
|
||||
sb.AppendLine("AI Reasoning:");
|
||||
sb.AppendLine(result.AIReasoning);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
await this.AppendAsync(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the final summary section and elapsed time to the log file.
|
||||
/// </summary>
|
||||
public async Task WriteSummaryAsync(
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
TimeSpan elapsed)
|
||||
{
|
||||
var passCount = orderedResults.Count(r => r.Passed);
|
||||
var failCount = orderedResults.Count(r => !r.Passed);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(new string('═', 72));
|
||||
sb.AppendLine("SUMMARY");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
sb.AppendLine($" {(result.Passed ? "✓" : "✗")} {result.SampleName}: {result.Summary}");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
sb.AppendLine($" ○ {name}: Skipped — {reason}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Results: {passCount} passed{(failCount > 0 ? $", {failCount} failed" : "")}{(skipped.Count > 0 ? $", {skipped.Count} skipped" : "")}");
|
||||
sb.AppendLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
||||
|
||||
await this.AppendAsync(sb.ToString());
|
||||
}
|
||||
|
||||
private async Task AppendAsync(string text)
|
||||
{
|
||||
await this._writeLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await File.AppendAllTextAsync(this._path, text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._writeLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Markdown summary of sample verification results.
|
||||
/// </summary>
|
||||
internal static class MarkdownResultWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the results to a Markdown file at the specified path.
|
||||
/// </summary>
|
||||
public static async Task WriteAsync(
|
||||
string path,
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
TimeSpan elapsed)
|
||||
{
|
||||
var passCount = orderedResults.Count(r => r.Passed);
|
||||
var failCount = orderedResults.Count(r => !r.Passed);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("# Sample Verification Results");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"**{passCount} passed, {failCount} failed, {skipped.Count} skipped** | Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Results table
|
||||
sb.AppendLine("| Sample | Status | Failed Checks | Failures |");
|
||||
sb.AppendLine("|--------|--------|---------------|----------|");
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
var status = result.Passed ? "✅ PASSED" : "❌ FAILED";
|
||||
var failedChecks = result.Failures.Count;
|
||||
var failures = MdEscape(string.Join("; ", result.Failures));
|
||||
sb.AppendLine($"| {MdEscape(result.SampleName)} | {status} | {failedChecks} | {failures} |");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
sb.AppendLine($"| {MdEscape(name)} | ⏭️ SKIPPED | 0 | {MdEscape(reason)} |");
|
||||
}
|
||||
|
||||
// Collapsible AI reasoning details for failures
|
||||
var failures2 = orderedResults.Where(r => !r.Passed && !string.IsNullOrEmpty(r.AIReasoning)).ToList();
|
||||
if (failures2.Count > 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## Failure Details");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var result in failures2)
|
||||
{
|
||||
sb.AppendLine($"<details><summary><strong>{HtmlEscape(result.SampleName)}</strong></summary>");
|
||||
sb.AppendLine();
|
||||
if (result.Failures.Count > 0)
|
||||
{
|
||||
foreach (var failure in result.Failures)
|
||||
{
|
||||
sb.AppendLine($"- {MdEscape(failure)}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("**AI Reasoning:**");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine(result.AIReasoning);
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("</details>");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes pipe characters and newlines for use inside Markdown table cells.
|
||||
/// </summary>
|
||||
private static string MdEscape(string value)
|
||||
{
|
||||
return value.Replace("|", "\\|").Replace("\n", " ").Replace("\r", "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes HTML special characters for use inside HTML tags.
|
||||
/// </summary>
|
||||
private static string HtmlEscape(string value)
|
||||
{
|
||||
return value.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This tool runs the 01-get-started, 02-agents, and 03-workflows samples and verifies their output.
|
||||
// Deterministic samples are verified with exact string matching.
|
||||
// Non-deterministic (LLM) samples are verified using an agent-framework agent.
|
||||
//
|
||||
// Usage:
|
||||
// dotnet run # Run all samples
|
||||
// dotnet run -- 01_hello_agent 05_first_workflow # Run specific samples by name
|
||||
// dotnet run -- --category 01-get-started # Run the 01-get-started category
|
||||
// dotnet run -- --category 02-agents # Run the 02-agents category
|
||||
// dotnet run -- --category 03-workflows # Run the 03-workflows category
|
||||
// dotnet run -- --parallel 16 # Run up to 16 samples concurrently
|
||||
// dotnet run -- --log results.log # Write sequential log to file
|
||||
// dotnet run -- --csv results.csv # Write CSV summary to file
|
||||
// dotnet run -- --md results.md # Write Markdown summary to file
|
||||
// dotnet run -- --build # Build samples during run (default: --no-build)
|
||||
// Note: By default, this tool expects sample build outputs to already exist.
|
||||
// Pre-build the solution before running, or pass --build to avoid missing build output failures.
|
||||
//
|
||||
// Required environment variables (for AI-powered samples):
|
||||
// AZURE_OPENAI_ENDPOINT
|
||||
// AZURE_OPENAI_DEPLOYMENT_NAME (optional, defaults to gpt-5-mini)
|
||||
|
||||
using System.Diagnostics;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using VerifySamples;
|
||||
|
||||
var options = VerifyOptions.Parse(args);
|
||||
if (options is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// Resolve the dotnet/ root directory (verify-samples is at dotnet/eng/verify-samples/)
|
||||
var dotnetRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
if (!File.Exists(Path.Combine(dotnetRoot, "agent-framework-dotnet.slnx")))
|
||||
{
|
||||
dotnetRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", ".."));
|
||||
}
|
||||
|
||||
// Set up the AI verifier
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5-mini";
|
||||
|
||||
OpenAI.Chat.ChatClient? chatClient = null;
|
||||
if (!string.IsNullOrEmpty(endpoint))
|
||||
{
|
||||
chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
}
|
||||
|
||||
// Set up optional log file writer
|
||||
LogFileWriter? logWriter = null;
|
||||
if (options.LogFilePath is not null)
|
||||
{
|
||||
logWriter = new LogFileWriter(options.LogFilePath);
|
||||
await logWriter.WriteHeaderAsync();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Run all samples
|
||||
var reporter = new ConsoleReporter();
|
||||
var verifier = new SampleVerifier(chatClient);
|
||||
var orchestrator = new VerificationOrchestrator(verifier, reporter, dotnetRoot, TimeSpan.FromMinutes(3), logWriter, buildSamples: options.BuildSamples);
|
||||
|
||||
var run = await orchestrator.RunAllAsync(options.Samples, options.MaxParallelism);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
// Print summary
|
||||
var orderedResults = run.SampleOrder
|
||||
.Where(run.Results.ContainsKey)
|
||||
.Select(name => run.Results[name])
|
||||
.ToList();
|
||||
|
||||
reporter.PrintSummary(orderedResults, run.Skipped, stopwatch.Elapsed);
|
||||
|
||||
// Write log file summary
|
||||
if (logWriter is not null)
|
||||
{
|
||||
await logWriter.WriteSummaryAsync(orderedResults, run.Skipped, stopwatch.Elapsed);
|
||||
Console.WriteLine($"Log written to: {options.LogFilePath}");
|
||||
}
|
||||
|
||||
// Write CSV summary
|
||||
if (options.CsvFilePath is not null)
|
||||
{
|
||||
await CsvResultWriter.WriteAsync(options.CsvFilePath, orderedResults, run.Skipped, options.Samples);
|
||||
Console.WriteLine($"CSV written to: {options.CsvFilePath}");
|
||||
}
|
||||
|
||||
// Write Markdown summary
|
||||
if (options.MarkdownFilePath is not null)
|
||||
{
|
||||
await MarkdownResultWriter.WriteAsync(options.MarkdownFilePath, orderedResults, run.Skipped, stopwatch.Elapsed);
|
||||
Console.WriteLine($"Markdown written to: {options.MarkdownFilePath}");
|
||||
}
|
||||
|
||||
return orderedResults.Any(r => !r.Passed) ? 1 : 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
logWriter?.Dispose();
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a sample to verify, including its expected output.
|
||||
/// </summary>
|
||||
internal sealed class SampleDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Display name for the sample (e.g., "01_hello_agent").
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Relative path from the dotnet/ directory to the sample project directory.
|
||||
/// </summary>
|
||||
public required string ProjectPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables that the sample requires for a meaningful run.
|
||||
/// The runner checks these before running and will skip the sample if any are unset,
|
||||
/// recording a skip reason that indicates which required variables are missing.
|
||||
/// </summary>
|
||||
public string[] RequiredEnvironmentVariables { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables that the sample can use but typically has fallbacks or defaults for.
|
||||
/// If these are not set, the sample might prompt or behave interactively, which could cause
|
||||
/// automated verification to hang. The runner checks these and skips the sample if they are unset
|
||||
/// to avoid non-deterministic or blocking behavior in automated runs.
|
||||
/// </summary>
|
||||
public string[] OptionalEnvironmentVariables { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// If set, the sample is skipped with this reason.
|
||||
/// Use only for structural reasons (e.g., web server, multi-process, needs external service).
|
||||
/// Do NOT use for missing environment variables — those are checked dynamically.
|
||||
/// </summary>
|
||||
public string? SkipReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Substrings that must appear in stdout for the sample to pass.
|
||||
/// Used for deterministic verification.
|
||||
/// </summary>
|
||||
public string[] MustContain { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Substrings that must not appear in stdout for the sample to pass.
|
||||
/// </summary>
|
||||
public string[] MustNotContain { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// If true, <see cref="MustContain"/> entries cover the entire expected output —
|
||||
/// no AI verification is needed.
|
||||
/// </summary>
|
||||
public bool IsDeterministic { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Natural-language description of what the sample output should look like.
|
||||
/// Used by the AI verifier for non-deterministic samples.
|
||||
/// Each entry describes one aspect of the expected output that should be verified.
|
||||
/// </summary>
|
||||
public string[] ExpectedOutputDescription { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Sequence of stdin inputs to feed to the sample process.
|
||||
/// Each entry is written as a line (followed by newline) to the process stdin.
|
||||
/// A <c>null</c> entry inserts a delay without writing anything.
|
||||
/// Inputs are sent with a short delay between each to allow the process to prompt.
|
||||
/// </summary>
|
||||
public string?[] Inputs { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Delay in milliseconds between each input line. Default is 2000ms.
|
||||
/// Increase for samples that need more time between prompts (e.g., LLM calls between inputs).
|
||||
/// </summary>
|
||||
public int InputDelayMs { get; init; } = 2000;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Result of running a sample process.
|
||||
/// </summary>
|
||||
internal sealed record SampleRunResult(
|
||||
string Stdout,
|
||||
string Stderr,
|
||||
int ExitCode,
|
||||
TimeSpan Elapsed);
|
||||
|
||||
/// <summary>
|
||||
/// Runs a sample project via <c>dotnet run</c> and captures its output.
|
||||
/// </summary>
|
||||
internal static class SampleRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs <c>dotnet run --framework net10.0</c> in the given project directory.
|
||||
/// When <paramref name="build"/> is false (the default), <c>--no-build</c> is passed
|
||||
/// to skip building, assuming the project was pre-built.
|
||||
/// </summary>
|
||||
public static Task<SampleRunResult> RunAsync(
|
||||
string projectPath,
|
||||
TimeSpan timeout,
|
||||
bool build = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> RunAsync(projectPath, DotnetRunArgs(build), timeout, inputs: null, inputDelayMs: 0, cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs <c>dotnet run --framework net10.0</c> with stdin inputs.
|
||||
/// When <paramref name="build"/> is false (the default), <c>--no-build</c> is passed
|
||||
/// to skip building, assuming the project was pre-built.
|
||||
/// </summary>
|
||||
public static Task<SampleRunResult> RunAsync(
|
||||
string projectPath,
|
||||
TimeSpan timeout,
|
||||
string?[]? inputs,
|
||||
int inputDelayMs = 2000,
|
||||
bool build = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> RunAsync(projectPath, DotnetRunArgs(build), timeout, inputs, inputDelayMs, cancellationToken);
|
||||
|
||||
private static string DotnetRunArgs(bool build) =>
|
||||
$"run {(build ? "" : "--no-build")} --framework net10.0";
|
||||
|
||||
/// <summary>
|
||||
/// Runs an arbitrary <c>dotnet</c> command in the given working directory.
|
||||
/// </summary>
|
||||
public static async Task<SampleRunResult> RunAsync(
|
||||
string workingDirectory,
|
||||
string dotnetArgs,
|
||||
TimeSpan timeout,
|
||||
string?[]? inputs = null,
|
||||
int inputDelayMs = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = dotnetArgs,
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = inputs is { Length: > 0 },
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
using var process = new Process { StartInfo = psi };
|
||||
process.Start();
|
||||
|
||||
var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
|
||||
// Feed stdin inputs with delays if configured
|
||||
if (inputs is { Length: > 0 })
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
await Task.Delay(inputDelayMs, cancellationToken);
|
||||
if (input is not null)
|
||||
{
|
||||
await process.StandardInput.WriteLineAsync(input.AsMemory(), cancellationToken);
|
||||
await process.StandardInput.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
process.StandardInput.Close();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or ObjectDisposedException or OperationCanceledException)
|
||||
{
|
||||
// Process may have exited before all inputs were sent
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(timeout);
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Timeout — kill the process
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
return new SampleRunResult(
|
||||
Stdout: await stdoutTask,
|
||||
Stderr: $"TIMEOUT: Sample did not complete within {timeout.TotalSeconds}s.\n{await stderrTask}",
|
||||
ExitCode: -1,
|
||||
Elapsed: sw.Elapsed);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
return new SampleRunResult(
|
||||
Stdout: await stdoutTask,
|
||||
Stderr: await stderrTask,
|
||||
ExitCode: process.ExitCode,
|
||||
Elapsed: sw.Elapsed);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user