Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1ee732ed |
@@ -1,19 +1,15 @@
|
||||
{
|
||||
"name": "Python 3",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3.14-bookworm",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
|
||||
"ghcr.io/devcontainers/features/copilot-cli:1": {}
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
|
||||
},
|
||||
"postCreateCommand": "bash ./devsetup.sh",
|
||||
"workspaceFolder": "/workspaces/agent-framework/python/",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"GitHub.copilot",
|
||||
"GitHub.vscode-github-actions",
|
||||
"ms-python.python",
|
||||
"ms-windows-ai-studio.windows-ai-studio",
|
||||
"littlefoxteam.vscode-python-test-adapter"
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
{
|
||||
"name": "C# (.NET)",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:9.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": {}
|
||||
},
|
||||
"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"
|
||||
|
||||
@@ -8,7 +8,7 @@ ignorePatterns:
|
||||
- pattern: "./blob"
|
||||
- pattern: "./issues"
|
||||
- pattern: "./discussions"
|
||||
- pattern: "./pull"
|
||||
- pattern: "./pulls"
|
||||
- pattern: "https:\/\/platform.openai.com"
|
||||
- pattern: "http:\/\/localhost"
|
||||
- pattern: "http:\/\/127.0.0.1"
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://aka.ms/agent-framework
|
||||
about: Check out the official documentation for guides and API reference.
|
||||
- name: Discussions
|
||||
url: https://github.com/microsoft/agent-framework/discussions
|
||||
about: Ask questions about Agent Framework.
|
||||
@@ -1,70 +0,0 @@
|
||||
name: .NET Bug Report
|
||||
description: Report a bug in the Agent Framework .NET SDK
|
||||
title: ".NET: [Bug]: "
|
||||
labels: ["bug", ".NET"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the bug.
|
||||
placeholder: |
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue.
|
||||
placeholder: |
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: error-messages
|
||||
attributes:
|
||||
label: Error Messages / Stack Traces
|
||||
description: Include any error messages or stack traces you received.
|
||||
placeholder: |
|
||||
```
|
||||
Paste error messages or stack traces here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: dotnet-packages
|
||||
attributes:
|
||||
label: Package Versions
|
||||
description: List the Microsoft.Agents.* packages and versions you are using
|
||||
placeholder: "e.g., Microsoft.Agents.AI.Abstractions: 1.0.0, Microsoft.Agents.AI.OpenAI: 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: dotnet-version
|
||||
attributes:
|
||||
label: .NET Version
|
||||
description: What version of .NET are you using?
|
||||
placeholder: "e.g., .NET 8.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots that might be helpful.
|
||||
placeholder: "Any additional information..."
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Request a new feature for Microsoft Agent Framework
|
||||
title: "[Feature]: "
|
||||
type: feature
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please describe the feature you'd like and why it would be useful.
|
||||
placeholder: |
|
||||
Describe the feature you're requesting:
|
||||
- What problem does it solve?
|
||||
- What would the expected behavior be?
|
||||
- Are there any alternatives you've considered?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a code sample showing how you'd like to use this feature.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```csharp
|
||||
// Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: language
|
||||
attributes:
|
||||
label: Language/SDK
|
||||
description: Which language/SDK does this feature apply to?
|
||||
options:
|
||||
- Both
|
||||
- .NET
|
||||
- Python
|
||||
- Other / Not Applicable
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Python Bug Report
|
||||
description: Report a bug in the Agent Framework Python SDK
|
||||
title: "Python: [Bug]: "
|
||||
labels: ["bug", "Python"]
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a clear and detailed description of the bug.
|
||||
placeholder: |
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: code-sample
|
||||
attributes:
|
||||
label: Code Sample
|
||||
description: If applicable, provide a minimal code sample that demonstrates the issue.
|
||||
placeholder: |
|
||||
```python
|
||||
# Your code here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: error-messages
|
||||
attributes:
|
||||
label: Error Messages / Stack Traces
|
||||
description: Include any error messages or stack traces you received.
|
||||
placeholder: |
|
||||
```
|
||||
Paste error messages or stack traces here
|
||||
```
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: python-packages
|
||||
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"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: python-version
|
||||
attributes:
|
||||
label: Python Version
|
||||
description: What version of Python are you using?
|
||||
placeholder: "e.g., Python 3.11"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots that might be helpful.
|
||||
placeholder: "Any additional information..."
|
||||
validations:
|
||||
required: false
|
||||
@@ -12,7 +12,7 @@ runs:
|
||||
docker rm -f dts-emulator
|
||||
fi
|
||||
echo "Starting Durable Task Scheduler Emulator"
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 -e DTS_USE_DYNAMIC_TASK_HUBS=true mcr.microsoft.com/dts/dts-emulator:latest
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
|
||||
echo "Waiting for Durable Task Scheduler Emulator to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
|
||||
echo "Durable Task Scheduler Emulator is ready"
|
||||
@@ -28,18 +28,6 @@ runs:
|
||||
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
|
||||
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
|
||||
echo "Azurite (Azure Storage emulator) is ready"
|
||||
- name: Start Redis
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$(docker ps -aq -f name=redis)" ]; then
|
||||
echo "Stopping and removing existing Redis"
|
||||
docker rm -f redis
|
||||
fi
|
||||
echo "Starting Redis"
|
||||
docker run -d --name redis -p 6379:6379 redis:latest
|
||||
echo "Waiting for Redis to be ready"
|
||||
timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
|
||||
echo "Redis is ready"
|
||||
- name: Install Azure Functions Core Tools
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Free runner disk space
|
||||
description: |
|
||||
Reclaims disk space on GitHub-hosted Ubuntu runners by removing
|
||||
pre-installed toolchains we do not use (Android SDK, GHC/Haskell,
|
||||
CodeQL bundle), Docker images, and swap. Also relocates the
|
||||
NuGet package cache to /mnt (which has ~75 GB free vs ~14 GB
|
||||
on /). No-op on non-Linux runners.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Free disk space (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "::group::Disk usage before cleanup"
|
||||
df -h /
|
||||
echo "::endgroup::"
|
||||
|
||||
# Remove pre-installed toolchains we never use on this repo's
|
||||
# dotnet/python jobs. These reclaim ~25-30 GB on ubuntu-latest.
|
||||
sudo rm -rf \
|
||||
/usr/local/lib/android \
|
||||
/usr/share/dotnet/sdk/NuGetFallbackFolder \
|
||||
/opt/ghc \
|
||||
/usr/local/.ghcup \
|
||||
/opt/hostedtoolcache/CodeQL \
|
||||
/opt/hostedtoolcache/PyPy \
|
||||
/opt/hostedtoolcache/Ruby \
|
||||
/opt/hostedtoolcache/go \
|
||||
/usr/local/share/boost \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/chromium \
|
||||
/usr/local/share/vcpkg \
|
||||
/usr/local/lib/heroku \
|
||||
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/PyPy" \
|
||||
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/Ruby" \
|
||||
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/go" || true
|
||||
|
||||
# Drop docker images shipped on the runner; jobs that need
|
||||
# docker pull what they need fresh.
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
sudo docker image prune --all --force >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Disable swap to free its backing file.
|
||||
sudo swapoff -a || true
|
||||
sudo rm -f /mnt/swapfile /swapfile || true
|
||||
|
||||
echo "::group::Disk usage after cleanup"
|
||||
df -h /
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Relocate NuGet package cache to /mnt (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo mkdir -p /mnt/nuget
|
||||
sudo chown -R "$USER":"$USER" /mnt/nuget
|
||||
echo "NUGET_PACKAGES=/mnt/nuget" >> "$GITHUB_ENV"
|
||||
echo "Relocated NuGet package cache to /mnt/nuget"
|
||||
df -h /mnt || true
|
||||
@@ -8,42 +8,18 @@ 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"
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
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)"
|
||||
if awk '/^\[tool\.uv\.workspace\]/{f=1;next} /^\[/{f=0} f && /^exclude = \[/{found=1} END{exit !found}' python/pyproject.toml; then
|
||||
if ! awk '/^\[tool\.uv\.workspace\]/{f=1;next} /^\[/{f=0} f && /^exclude = \[/ && index($0, "\"'"$pkg_dir"'\"")' python/pyproject.toml | grep -q .; then
|
||||
sed -i.bak '/\[tool\.uv\.workspace\]/,/^\[/ { /^exclude = \[/ s|\]|, "'"$pkg_dir"'"]| }' python/pyproject.toml
|
||||
fi
|
||||
else
|
||||
sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml
|
||||
fi
|
||||
sed -i.bak '/'"$pkg"' = { workspace = true }/d' python/pyproject.toml
|
||||
fi
|
||||
done
|
||||
done
|
||||
- name: Install the project
|
||||
shell: bash
|
||||
run: |
|
||||
cd python && uv sync --all-packages --all-extras --dev --prerelease=if-necessary-or-explicit
|
||||
cd python && uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
|
||||
|
||||
@@ -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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ inputs.azure-client-id }}
|
||||
tenant-id: ${{ inputs.azure-tenant-id }}
|
||||
subscription-id: ${{ inputs.azure-subscription-id }}
|
||||
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
os: ${{ inputs.os }}
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,9 @@ updates:
|
||||
schedule:
|
||||
interval: "cron"
|
||||
cronjob: "0 8 * * 4,0" # Every Thursday(4) and Sunday(0) at 8:00 UTC
|
||||
experimental:
|
||||
nuget-native-updater: false
|
||||
enable-cooldown-metrics-collection: false
|
||||
ignore:
|
||||
# For all System.* and Microsoft.Extensions/Bcl.* packages, ignore all major version updates
|
||||
- dependency-name: "System.*"
|
||||
@@ -25,14 +28,6 @@ updates:
|
||||
- "dependencies"
|
||||
|
||||
# Maintain dependencies for python
|
||||
- package-ecosystem: "pip"
|
||||
directory: "python/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
labels:
|
||||
- "python"
|
||||
- "dependencies"
|
||||
- package-ecosystem: "uv"
|
||||
directory: "python/"
|
||||
schedule:
|
||||
@@ -44,15 +39,9 @@ updates:
|
||||
|
||||
# Maintain dependencies for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
# Cover both the standard workflow location and our composite actions.
|
||||
# With `directory: "/"` Dependabot only scans `.github/workflows/*.{yml,yaml}`
|
||||
# plus a root-level `action.yml/action.yaml`. It does NOT recurse into
|
||||
# `.github/actions/*/action.yml`, so the glob below is required to keep the
|
||||
# composite actions in `.github/actions/<name>/` up to date as well.
|
||||
# Ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#directories-or-directory--
|
||||
directories:
|
||||
- "/"
|
||||
- "/.github/actions/*"
|
||||
# Workflow files stored in the
|
||||
# default location of `.github/workflows`
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "sunday"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
applyTo: "dotnet/src/Microsoft.Agents.AI.DurableTask/**,dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**"
|
||||
---
|
||||
|
||||
# Durable Task area code instructions
|
||||
|
||||
The following guidelines apply to pull requests that modify files under
|
||||
`dotnet/src/Microsoft.Agents.AI.DurableTask/**` or
|
||||
`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**`:
|
||||
|
||||
## CHANGELOG.md
|
||||
|
||||
- Each pull request that modifies code should add just one bulleted entry to the `CHANGELOG.md` file containing a change title (usually the PR title) and a link to the PR itself.
|
||||
- New PRs should be added to the top of the `CHANGELOG.md` file under a "## [Unreleased]" heading.
|
||||
- If the PR is the first since the last release, the existing "## [Unreleased]" heading should be replaced with a "## v[X.Y.Z]" heading and the PRs since the last release should be added to the new "## [Unreleased]" heading.
|
||||
- The style of new `CHANGELOG.md` entries should match the style of the other entries in the file.
|
||||
- If the PR introduces a breaking change, the changelog entry should be prefixed with "[BREAKING]".
|
||||
@@ -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,181 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
function getPullRequest(context) {
|
||||
const pullRequest = context.payload.pull_request;
|
||||
if (!pullRequest?.number || !pullRequest.user?.login) {
|
||||
throw new Error('This script must be run from a pull_request_target event.');
|
||||
}
|
||||
|
||||
return {
|
||||
author: pullRequest.user.login,
|
||||
authorType: pullRequest.user.type,
|
||||
labels: pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [],
|
||||
number: pullRequest.number,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureLabel({ github, owner, repo, labelName }) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: labelName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: labelName,
|
||||
color: 'd93f0b',
|
||||
description: 'Community author has exceeded the open pull request limit.',
|
||||
});
|
||||
} catch (createError) {
|
||||
if (createError.status !== 422) {
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasLabel(labels, labelName) {
|
||||
if (!labelName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return labels.some((label) => label.toLowerCase() === labelName.toLowerCase());
|
||||
}
|
||||
|
||||
function isDependabotAuthor({ author, authorType }) {
|
||||
return authorType === 'Bot' && author.toLowerCase() === 'dependabot[bot]';
|
||||
}
|
||||
|
||||
function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) {
|
||||
return [
|
||||
`Thank you for your contribution, @${author}.`,
|
||||
'',
|
||||
`To keep the review queue manageable, we currently limit community contributors to ${maxOpenPrs} `
|
||||
+ `open pull requests at a time. This PR would put you at ${openPrCount} open pull requests, `
|
||||
+ 'so we are closing it automatically.',
|
||||
'',
|
||||
'Please focus on getting your existing PRs reviewed, merged, or closed before opening another one. '
|
||||
+ `If a maintainer asked you to open this PR, they can apply the \`${exemptLabelName}\` label and reopen it.`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber }) {
|
||||
const openPullRequests = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const authorOpenPullRequestNumbers = openPullRequests
|
||||
.filter((pullRequest) => pullRequest.user?.login === author)
|
||||
.map((pullRequest) => pullRequest.number);
|
||||
const currentPrIsOpen = authorOpenPullRequestNumbers.includes(pullRequestNumber);
|
||||
const existingOpenPrCount = currentPrIsOpen
|
||||
? authorOpenPullRequestNumbers.length - 1
|
||||
: authorOpenPullRequestNumbers.length;
|
||||
|
||||
return existingOpenPrCount + 1;
|
||||
}
|
||||
|
||||
async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const { author, authorType, labels, number } = getPullRequest(context);
|
||||
|
||||
if (isDependabotAuthor({ author, authorType })) {
|
||||
core.info(`Author ${author} is Dependabot; skipping open PR limit enforcement.`);
|
||||
return {
|
||||
author,
|
||||
closed: false,
|
||||
dependabotExempt: true,
|
||||
openPrCount: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasLabel(labels, exemptLabelName)) {
|
||||
core.info(`PR #${number} has the ${exemptLabelName} label; skipping open PR limit enforcement.`);
|
||||
return {
|
||||
author,
|
||||
closed: false,
|
||||
exempt: true,
|
||||
openPrCount: null,
|
||||
};
|
||||
}
|
||||
|
||||
const openPrCount = await getOpenPrCount({
|
||||
github,
|
||||
owner,
|
||||
repo,
|
||||
author,
|
||||
pullRequestNumber: number,
|
||||
});
|
||||
|
||||
if (openPrCount <= maxOpenPrs) {
|
||||
core.info(
|
||||
`${author} has ${openPrCount} open pull request(s), which is within the limit of ${maxOpenPrs}.`,
|
||||
);
|
||||
return {
|
||||
author,
|
||||
closed: false,
|
||||
openPrCount,
|
||||
};
|
||||
}
|
||||
|
||||
await ensureLabel({
|
||||
github,
|
||||
owner,
|
||||
repo,
|
||||
labelName,
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
labels: [labelName],
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
body: buildLimitMessage({
|
||||
author,
|
||||
exemptLabelName,
|
||||
maxOpenPrs,
|
||||
openPrCount,
|
||||
}),
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: number,
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
core.info(
|
||||
`${author} has ${openPrCount} open pull request(s), which exceeds the limit of ${maxOpenPrs}. `
|
||||
+ `Closed PR #${number}.`,
|
||||
);
|
||||
|
||||
return {
|
||||
author,
|
||||
closed: true,
|
||||
openPrCount,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildLimitMessage,
|
||||
enforcePrLimit,
|
||||
getOpenPrCount,
|
||||
};
|
||||
@@ -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,341 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/**
|
||||
* Tests for pr_limit_moderation.js.
|
||||
*
|
||||
* Run with: node --test .github/tests/test_pr_limit_moderation.js
|
||||
*/
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { enforcePrLimit } = require('../scripts/pr_limit_moderation.js');
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createContext({ author = 'community-user', authorType = 'User', labels = [], number = 123 } = {}) {
|
||||
return {
|
||||
repo: {
|
||||
owner: 'microsoft',
|
||||
repo: 'agent-framework',
|
||||
},
|
||||
payload: {
|
||||
pull_request: {
|
||||
number,
|
||||
labels: labels.map((name) => ({ name })),
|
||||
user: {
|
||||
login: author,
|
||||
type: authorType,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createCore() {
|
||||
const messages = [];
|
||||
return {
|
||||
messages,
|
||||
info(message) {
|
||||
messages.push(message);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createGithub({
|
||||
itemNumbers,
|
||||
labelExists = true,
|
||||
pullRequests = createPullRequestPage({ numbers: itemNumbers }),
|
||||
}) {
|
||||
const calls = [];
|
||||
|
||||
return {
|
||||
calls,
|
||||
async paginate(method, params) {
|
||||
calls.push({ api: 'paginate', method, params });
|
||||
return pullRequests;
|
||||
},
|
||||
rest: {
|
||||
issues: {
|
||||
async getLabel(params) {
|
||||
calls.push({ api: 'issues.getLabel', params });
|
||||
if (!labelExists) {
|
||||
const error = new Error('Not Found');
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
return { data: { name: params.name } };
|
||||
},
|
||||
async createLabel(params) {
|
||||
calls.push({ api: 'issues.createLabel', params });
|
||||
return { data: { name: params.name } };
|
||||
},
|
||||
async addLabels(params) {
|
||||
calls.push({ api: 'issues.addLabels', params });
|
||||
return { data: [] };
|
||||
},
|
||||
async createComment(params) {
|
||||
calls.push({ api: 'issues.createComment', params });
|
||||
return { data: { id: 1 } };
|
||||
},
|
||||
},
|
||||
pulls: {
|
||||
async list(params) {
|
||||
calls.push({ api: 'pulls.list', params });
|
||||
return { data: pullRequests };
|
||||
},
|
||||
async update(params) {
|
||||
calls.push({ api: 'pulls.update', params });
|
||||
return { data: { state: params.state } };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPullRequestPage({ author = 'community-user', numbers }) {
|
||||
return numbers.map((number) => ({
|
||||
number,
|
||||
user: {
|
||||
login: author,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PR limit enforcement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PR limit enforcement', () => {
|
||||
it('does not close the PR when the author is at the open PR limit', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 123],
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext(),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, false);
|
||||
assert.equal(result.openPrCount, 10);
|
||||
assert.deepEqual(
|
||||
github.calls.map((call) => call.api),
|
||||
['paginate'],
|
||||
);
|
||||
});
|
||||
|
||||
it('counts the new PR when the pull list includes it', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [123, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext(),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, true);
|
||||
assert.equal(result.openPrCount, 11);
|
||||
assert.deepEqual(
|
||||
github.calls.map((call) => call.api),
|
||||
[
|
||||
'paginate',
|
||||
'issues.getLabel',
|
||||
'issues.addLabels',
|
||||
'issues.createComment',
|
||||
'pulls.update',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('counts the current PR on top of existing open PRs', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [123, ...Array.from({ length: 24 }, (_, index) => index + 1)],
|
||||
pullRequests: createPullRequestPage({
|
||||
numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)],
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext(),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, true);
|
||||
assert.equal(result.openPrCount, 26);
|
||||
const comment = github.calls.find((call) => call.api === 'issues.createComment').params.body;
|
||||
assert.match(comment, /This PR would put you at 26 open pull requests/);
|
||||
});
|
||||
|
||||
it('creates the label when it does not already exist', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||
labelExists: false,
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext(),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, true);
|
||||
assert.deepEqual(
|
||||
github.calls.map((call) => call.api),
|
||||
[
|
||||
'paginate',
|
||||
'issues.getLabel',
|
||||
'issues.createLabel',
|
||||
'issues.addLabels',
|
||||
'issues.createComment',
|
||||
'pulls.update',
|
||||
],
|
||||
);
|
||||
assert.equal(
|
||||
github.calls.find((call) => call.api === 'issues.createLabel').params.name,
|
||||
'too-many-prs',
|
||||
);
|
||||
});
|
||||
|
||||
it('tolerates a 422 race when creating the label', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||
labelExists: false,
|
||||
});
|
||||
github.rest.issues.createLabel = async (params) => {
|
||||
github.calls.push({ api: 'issues.createLabel', params });
|
||||
const error = new Error('Validation Failed');
|
||||
error.status = 422;
|
||||
throw error;
|
||||
};
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext(),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, true);
|
||||
assert.deepEqual(
|
||||
github.calls.map((call) => call.api),
|
||||
[
|
||||
'paginate',
|
||||
'issues.getLabel',
|
||||
'issues.createLabel',
|
||||
'issues.addLabels',
|
||||
'issues.createComment',
|
||||
'pulls.update',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a diplomatic close message with the configured limit', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||
pullRequests: createPullRequestPage({
|
||||
author: 'octo-contributor',
|
||||
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||
}),
|
||||
});
|
||||
|
||||
await enforcePrLimit({
|
||||
github,
|
||||
context: createContext({ author: 'octo-contributor' }),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
const comment = github.calls.find((call) => call.api === 'issues.createComment').params.body;
|
||||
assert.match(comment, /Thank you for your contribution/);
|
||||
assert.match(comment, /limit community contributors to 10 open pull requests/);
|
||||
assert.match(comment, /@octo-contributor/);
|
||||
assert.match(comment, /`pr-limit-exempt` label and reopen/);
|
||||
});
|
||||
|
||||
it('does not close an exempt PR when it is reopened', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext({ labels: ['PR-LIMIT-EXEMPT'] }),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, false);
|
||||
assert.equal(result.exempt, true);
|
||||
assert.equal(result.openPrCount, null);
|
||||
assert.deepEqual(github.calls, []);
|
||||
});
|
||||
|
||||
it('does not close Dependabot PRs', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)],
|
||||
pullRequests: createPullRequestPage({
|
||||
author: 'dependabot[bot]',
|
||||
numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)],
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext({ author: 'dependabot[bot]', authorType: 'Bot' }),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, false);
|
||||
assert.equal(result.dependabotExempt, true);
|
||||
assert.equal(result.openPrCount, null);
|
||||
assert.deepEqual(github.calls, []);
|
||||
});
|
||||
|
||||
it('counts the current PR when the author has more than one page of open PRs', async () => {
|
||||
const github = createGithub({
|
||||
itemNumbers: [123, ...Array.from({ length: 100 }, (_, index) => index + 1)],
|
||||
});
|
||||
|
||||
const result = await enforcePrLimit({
|
||||
github,
|
||||
context: createContext({ number: 123 }),
|
||||
core: createCore(),
|
||||
exemptLabelName: 'pr-limit-exempt',
|
||||
maxOpenPrs: 10,
|
||||
labelName: 'too-many-prs',
|
||||
});
|
||||
|
||||
assert.equal(result.closed, true);
|
||||
assert.equal(result.openPrCount, 101);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
@@ -105,7 +105,7 @@ After completing migration, verify these specific items:
|
||||
1. **Compilation**: Execute `dotnet build` on all modified projects - zero errors required
|
||||
2. **Namespace Updates**: Confirm all `using Microsoft.SemanticKernel.Agents` statements are replaced
|
||||
3. **Method Calls**: Verify all `InvokeAsync` calls are changed to `RunAsync`
|
||||
4. **Return Types**: Confirm handling of `AgentResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
4. **Return Types**: Confirm handling of `AgentRunResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
5. **Thread Creation**: Validate all thread creation uses `agent.GetNewThread()` pattern
|
||||
6. **Tool Registration**: Ensure `[KernelFunction]` attributes are removed and `AIFunctionFactory.Create()` is used
|
||||
7. **Options Configuration**: Verify `AgentRunOptions` or `ChatClientAgentRunOptions` replaces `AgentInvokeOptions`
|
||||
@@ -119,7 +119,7 @@ Agent Framework provides functionality for creating and managing AI agents throu
|
||||
Key API differences:
|
||||
- Agent creation: Remove Kernel dependency, use direct client-based creation
|
||||
- Method names: `InvokeAsync` → `RunAsync`, `InvokeStreamingAsync` → `RunStreamingAsync`
|
||||
- Return types: `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentResponse`
|
||||
- Return types: `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentRunResponse`
|
||||
- Thread creation: Provider-specific constructors → `agent.GetNewThread()`
|
||||
- Tool registration: `KernelPlugin` system → Direct `AIFunction` registration
|
||||
- Options: `AgentInvokeOptions` → Provider-specific run options (e.g., `ChatClientAgentRunOptions`)
|
||||
@@ -142,9 +142,9 @@ Replace these Semantic Kernel agent classes with their Agent Framework equivalen
|
||||
|----------------------|----------------------------|-------------------|
|
||||
| `IChatCompletionService` | `IChatClient` | Convert to `IChatClient` using `chatService.AsChatClient()` extensions |
|
||||
| `ChatCompletionAgent` | `ChatClientAgent` | Remove `Kernel` parameter, add `IChatClient` parameter |
|
||||
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | ⚠️ **Deprecated** - Use Responses API instead. <br> **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
|
||||
| `OpenAIAssistantAgent` | `AIAgent` (via extension) | **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` <br> **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
|
||||
| `AzureAIAgent` | `AIAgent` (via extension) | **New**: `PersistentAgentsClient.CreateAIAgent()` <br> **Existing**: `PersistentAgentsClient.GetAIAgent(agentId)` |
|
||||
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient(modelId).CreateAIAgent()` |
|
||||
| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient().CreateAIAgent()` |
|
||||
| `A2AAgent` | `AIAgent` (via extension) | Replace with `A2ACardResolver.GetAIAgentAsync()` |
|
||||
| `BedrockAgent` | Not supported | Custom implementation required |
|
||||
|
||||
@@ -166,8 +166,8 @@ Replace these method calls:
|
||||
| `thread.DeleteAsync()` | Provider-specific cleanup | Use provider client directly |
|
||||
|
||||
Return type changes:
|
||||
- `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentResponse`
|
||||
- `IAsyncEnumerable<StreamingChatMessageContent>` → `IAsyncEnumerable<AgentResponseUpdate>`
|
||||
- `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` → `AgentRunResponse`
|
||||
- `IAsyncEnumerable<StreamingChatMessageContent>` → `IAsyncEnumerable<AgentRunResponseUpdate>`
|
||||
</api_changes>
|
||||
|
||||
<configuration_changes>
|
||||
@@ -191,8 +191,8 @@ Agent Framework changes these behaviors compared to Semantic Kernel Agents:
|
||||
1. **Thread Management**: Agent Framework automatically manages thread state. Semantic Kernel required manual thread updates in some scenarios (e.g., OpenAI Responses).
|
||||
|
||||
2. **Return Types**:
|
||||
- Non-streaming: Returns single `AgentResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
- Streaming: Returns `IAsyncEnumerable<AgentResponseUpdate>` instead of `IAsyncEnumerable<StreamingChatMessageContent>`
|
||||
- Non-streaming: Returns single `AgentRunResponse` instead of `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>`
|
||||
- Streaming: Returns `IAsyncEnumerable<AgentRunResponseUpdate>` instead of `IAsyncEnumerable<StreamingChatMessageContent>`
|
||||
|
||||
3. **Tool Registration**: Agent Framework uses direct function registration without requiring `[KernelFunction]` attributes.
|
||||
|
||||
@@ -397,7 +397,7 @@ await foreach (AgentResponseItem<ChatMessageContent> item in agent.InvokeAsync(u
|
||||
|
||||
**With this Agent Framework non-streaming pattern:**
|
||||
```csharp
|
||||
AgentResponse result = await agent.RunAsync(userInput, thread, options);
|
||||
AgentRunResponse result = await agent.RunAsync(userInput, thread, options);
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
|
||||
@@ -411,7 +411,7 @@ await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync(
|
||||
|
||||
**With this Agent Framework streaming pattern:**
|
||||
```csharp
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
@@ -420,8 +420,8 @@ await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userInput,
|
||||
**Required changes:**
|
||||
1. Replace `agent.InvokeAsync()` with `agent.RunAsync()`
|
||||
2. Replace `agent.InvokeStreamingAsync()` with `agent.RunStreamingAsync()`
|
||||
3. Change return type handling from `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` to `AgentResponse`
|
||||
4. Change streaming type from `StreamingChatMessageContent` to `AgentResponseUpdate`
|
||||
3. Change return type handling from `IAsyncEnumerable<AgentResponseItem<ChatMessageContent>>` to `AgentRunResponse`
|
||||
4. Change streaming type from `StreamingChatMessageContent` to `AgentRunResponseUpdate`
|
||||
5. Remove `await foreach` for non-streaming calls
|
||||
6. Access message content directly from result object instead of iterating
|
||||
</api_changes>
|
||||
@@ -529,14 +529,14 @@ AIAgent agent = new OpenAIClient(apiKey)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**OpenAI Assistants (New):** ⚠️ *Deprecated - Use Responses API instead*
|
||||
**OpenAI Assistants (New):**
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetAssistantClient()
|
||||
.CreateAIAgent(modelId, instructions: instructions);
|
||||
```
|
||||
|
||||
**OpenAI Assistants (Existing):** ⚠️ *Deprecated - Use Responses API instead*
|
||||
**OpenAI Assistants (Existing):**
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetAssistantClient()
|
||||
@@ -562,20 +562,6 @@ AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
|
||||
.GetAIAgentAsync(agentId);
|
||||
```
|
||||
|
||||
**OpenAI Responses:** *(Recommended for OpenAI)*
|
||||
```csharp
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetOpenAIResponseClient(modelId)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**Azure OpenAI Responses:** *(Recommended for Azure OpenAI)*
|
||||
```csharp
|
||||
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**A2A:**
|
||||
```csharp
|
||||
A2ACardResolver resolver = new(new Uri(agentHost));
|
||||
@@ -661,7 +647,7 @@ await foreach (var result in agent.InvokeAsync(input, thread, options))
|
||||
```csharp
|
||||
ChatClientAgentRunOptions options = new(new ChatOptions { MaxOutputTokens = 1000 });
|
||||
|
||||
AgentResponse result = await agent.RunAsync(input, thread, options);
|
||||
AgentRunResponse result = await agent.RunAsync(input, thread, options);
|
||||
Console.WriteLine(result);
|
||||
|
||||
// Access underlying content when needed:
|
||||
@@ -689,7 +675,7 @@ await foreach (var result in agent.InvokeAsync(input, thread, options))
|
||||
|
||||
**With this Agent Framework non-streaming usage pattern:**
|
||||
```csharp
|
||||
AgentResponse result = await agent.RunAsync(input, thread, options);
|
||||
AgentRunResponse result = await agent.RunAsync(input, thread, options);
|
||||
Console.WriteLine($"Tokens: {result.Usage.TotalTokenCount}");
|
||||
```
|
||||
|
||||
@@ -709,7 +695,7 @@ await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsyn
|
||||
|
||||
**With this Agent Framework streaming usage pattern:**
|
||||
```csharp
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
|
||||
{
|
||||
if (update.Contents.OfType<UsageContent>().FirstOrDefault() is { } usageContent)
|
||||
{
|
||||
@@ -776,57 +762,35 @@ await foreach (var content in agent.InvokeAsync(userInput, thread))
|
||||
|
||||
**With this Agent Framework CodeInterpreter pattern:**
|
||||
```csharp
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var result = await agent.RunAsync(userInput, thread);
|
||||
Console.WriteLine(result);
|
||||
|
||||
// Get the CodeInterpreterToolCallContent (code input)
|
||||
CodeInterpreterToolCallContent? toolCallContent = result.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<CodeInterpreterToolCallContent>()
|
||||
.FirstOrDefault();
|
||||
// Extract chat response MEAI type via first level breaking glass
|
||||
var chatResponse = result.RawRepresentation as ChatResponse;
|
||||
|
||||
if (toolCallContent?.Inputs is not null)
|
||||
// Extract underlying SDK updates via second level breaking glass
|
||||
var underlyingStreamingUpdates = chatResponse?.RawRepresentation as IEnumerable<object?> ?? [];
|
||||
|
||||
StringBuilder generatedCode = new();
|
||||
foreach (object? underlyingUpdate in underlyingStreamingUpdates ?? [])
|
||||
{
|
||||
DataContent? codeInput = toolCallContent.Inputs.OfType<DataContent>().FirstOrDefault();
|
||||
if (codeInput?.HasTopLevelMediaType("text") ?? false)
|
||||
if (underlyingUpdate is RunStepDetailsUpdate stepDetailsUpdate && stepDetailsUpdate.CodeInterpreterInput is not null)
|
||||
{
|
||||
Console.WriteLine($"Code Input: {Encoding.UTF8.GetString(codeInput.Data.ToArray())}");
|
||||
generatedCode.Append(stepDetailsUpdate.CodeInterpreterInput);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the CodeInterpreterToolResultContent (code output)
|
||||
CodeInterpreterToolResultContent? toolResultContent = result.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<CodeInterpreterToolResultContent>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (toolResultContent?.Outputs is not null)
|
||||
if (!string.IsNullOrEmpty(generatedCode.ToString()))
|
||||
{
|
||||
TextContent? resultOutput = toolResultContent.Outputs.OfType<TextContent>().FirstOrDefault();
|
||||
if (resultOutput is not null)
|
||||
{
|
||||
Console.WriteLine($"Code Tool Result: {resultOutput.Text}");
|
||||
}
|
||||
}
|
||||
|
||||
// Getting any annotations generated by the tool
|
||||
foreach (AIAnnotation annotation in result.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.SelectMany(c => c.Annotations ?? []))
|
||||
{
|
||||
Console.WriteLine($"Annotation: {annotation}");
|
||||
Console.WriteLine($"\n# {chatResponse?.Messages[0].Role}:Generated Code:\n{generatedCode}");
|
||||
}
|
||||
```
|
||||
|
||||
**Functional differences:**
|
||||
1. Code interpreter content is now available via MEAI abstractions - no breaking glass required
|
||||
2. Use `CodeInterpreterToolCallContent` to access code inputs (the generated code)
|
||||
3. Use `CodeInterpreterToolResultContent` to access code outputs (execution results)
|
||||
4. Annotations are accessible via `AIAnnotation` on content items
|
||||
1. Code interpreter output is separate from text content, not a metadata property
|
||||
2. Access code via `RunStepDetailsUpdate.CodeInterpreterInput` instead of metadata
|
||||
3. Use breaking glass pattern to access underlying SDK objects
|
||||
4. Process text content and code interpreter output independently
|
||||
</behavioral_changes>
|
||||
|
||||
#### Provider-Specific Options Configuration
|
||||
@@ -839,7 +803,7 @@ var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 8000,
|
||||
// Breaking glass to access provider-specific options
|
||||
RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
|
||||
RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions()
|
||||
{
|
||||
ReasoningOptions = new()
|
||||
{
|
||||
@@ -1016,8 +980,6 @@ AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential(
|
||||
|
||||
### 3. OpenAI Assistants Migration
|
||||
|
||||
> ⚠️ **DEPRECATION WARNING**: The OpenAI Assistants API has been deprecated. The Agent Framework extension methods for Assistants are marked as `[Obsolete]`. **Please use the Responses API instead** (see Section 6: OpenAI Responses Migration).
|
||||
|
||||
<configuration_changes>
|
||||
**Remove Semantic Kernel Packages:**
|
||||
```xml
|
||||
@@ -1329,7 +1291,52 @@ var result = await agent.RunAsync(userInput, thread);
|
||||
```
|
||||
</api_changes>
|
||||
|
||||
### 8. Unsupported Providers (Require Custom Implementation)
|
||||
### 8. A2A Migration
|
||||
|
||||
<configuration_changes>
|
||||
**Remove Semantic Kernel Packages:**
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.A2A" />
|
||||
```
|
||||
|
||||
**Add Agent Framework Packages:**
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.Agents.AI.A2A" />
|
||||
```
|
||||
</configuration_changes>
|
||||
|
||||
<api_changes>
|
||||
**Replace this Semantic Kernel pattern:**
|
||||
```csharp
|
||||
using A2A;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.A2A;
|
||||
|
||||
using var httpClient = CreateHttpClient();
|
||||
var client = new A2AClient(agentUrl, httpClient);
|
||||
var cardResolver = new A2ACardResolver(url, httpClient);
|
||||
var agentCard = await cardResolver.GetAgentCardAsync();
|
||||
Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
|
||||
var agent = new A2AAgent(client, agentCard);
|
||||
```
|
||||
|
||||
**With this Agent Framework pattern:**
|
||||
```csharp
|
||||
using System;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.A2A;
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
|
||||
AIAgent agent = await agentCardResolver.GetAIAgentAsync();
|
||||
```
|
||||
</api_changes>
|
||||
|
||||
### 9. Unsupported Providers (Require Custom Implementation)
|
||||
|
||||
<behavioral_changes>
|
||||
#### BedrockAgent Migration
|
||||
@@ -1500,7 +1507,7 @@ Console.WriteLine(result);
|
||||
```
|
||||
</behavioral_changes>
|
||||
|
||||
### 9. Function Invocation Filtering
|
||||
### 10. Function Invocation Filtering
|
||||
|
||||
**Invocation Context**
|
||||
|
||||
@@ -1608,4 +1615,25 @@ var filteredAgent = originalAgent
|
||||
.Build();
|
||||
```
|
||||
|
||||
### 11. Function Invocation Contexts
|
||||
|
||||
**Invocation Context**
|
||||
|
||||
Semantic Kernel's `IAutoFunctionInvocationFilter` provides a `AutoFunctionInvocationContext` where Agent Framework provides `FunctionInvocationContext`
|
||||
|
||||
The property mapping guide from a `AutoFunctionInvocationContext` to a `FunctionInvocationContext` is as follows:
|
||||
|
||||
| Semantic Kernel | Agent Framework |
|
||||
| --- | --- |
|
||||
| RequestSequenceIndex | Iteration |
|
||||
| FunctionSequenceIndex | FunctionCallIndex |
|
||||
| ToolCallId | CallContent.CallId |
|
||||
| ChatMessageContent | Messages[0] |
|
||||
| ExecutionSettings | Options |
|
||||
| ChatHistory | Messages |
|
||||
| Function | Function |
|
||||
| Kernel | N/A |
|
||||
| Result | Use `return` from the delegate |
|
||||
| Terminate | Terminate |
|
||||
| CancellationToken | provided via argument to middleware delegate |
|
||||
| Arguments | Arguments |
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -64,6 +64,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -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@ed597411d8f924073f98dfc5c65a23a2325f34cd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # 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
|
||||
@@ -18,7 +18,6 @@ on:
|
||||
|
||||
env:
|
||||
COVERAGE_THRESHOLD: 80
|
||||
COVERAGE_FRAMEWORK: net10.0 # framework target for which we run/report code coverage
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -35,97 +34,51 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
|
||||
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
|
||||
foundryHostingChanges: ${{ steps.filter.outputs.foundryHosting }}
|
||||
functionsChanged: ${{ steps.filter.outputs.functions }}
|
||||
coreChanged: ${{ steps.filter.outputs.core }}
|
||||
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
dotnet:
|
||||
- 'dotnet/**'
|
||||
cosmosdb:
|
||||
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
|
||||
# The Foundry hosted-agent IT is costly (builds a container, pushes to ACR,
|
||||
# provisions live agents). Only run it when the project under test, its
|
||||
# dependency chain, the test container, the test fixture, or their tooling
|
||||
# changed. Keep this list in sync with $hashedDirs in scripts/it-build-image.ps1.
|
||||
foundryHosting:
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Foundry/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Workflows/**'
|
||||
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/**'
|
||||
- 'dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/**'
|
||||
- 'dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/**'
|
||||
- 'dotnet/Directory.Packages.props'
|
||||
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1'
|
||||
- '.github/workflows/dotnet-build-and-test.yml'
|
||||
functions:
|
||||
- 'dotnet/src/Microsoft.Agents.AI.DurableTask/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**'
|
||||
- 'dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/**'
|
||||
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/**'
|
||||
- '.github/actions/azure-functions-integration-setup/**'
|
||||
- '.github/workflows/dotnet-build-and-test.yml'
|
||||
core:
|
||||
- 'dotnet/src/Microsoft.Agents.AI/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.OpenAI/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Workflows/**'
|
||||
- 'dotnet/src/Microsoft.Agents.AI.Workflows.Generators/**'
|
||||
- 'dotnet/eng/scripts/New-FilteredSolution.ps1'
|
||||
- 'dotnet/tests/Directory.Build.props'
|
||||
- 'dotnet/Directory.Packages.props'
|
||||
- 'dotnet/global.json'
|
||||
- '.github/workflows/dotnet-build-and-test.yml'
|
||||
# run only if 'dotnet' files were changed
|
||||
- name: dotnet tests
|
||||
if: steps.filter.outputs.dotnet == 'true'
|
||||
run: echo "Dotnet file"
|
||||
- name: dotnet CosmosDB tests
|
||||
if: steps.filter.outputs.cosmosdb == 'true'
|
||||
run: echo "Dotnet CosmosDB changes"
|
||||
# run only if not 'dotnet' files were changed
|
||||
- name: not dotnet tests
|
||||
if: steps.filter.outputs.dotnet != 'true'
|
||||
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: "net9.0", os: "windows-latest", configuration: Debug }
|
||||
- { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release }
|
||||
- { targetFramework: "net472", os: "windows-latest", configuration: Release }
|
||||
- { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
|
||||
- { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Debug }
|
||||
- { targetFramework: "net9.0", 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Free runner disk space
|
||||
uses: ./.github/actions/free-runner-disk-space
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
uses: actions/setup-dotnet@v5.0.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
- name: Build dotnet solutions
|
||||
@@ -170,381 +123,98 @@ jobs:
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Free runner disk space
|
||||
uses: ./.github/actions/free-runner-disk-space
|
||||
|
||||
# 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@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # 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
|
||||
- name: Run Unit Tests Windows
|
||||
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 `
|
||||
-TestProjectNameIncludeFilter "*UnitTests*" `
|
||||
-OutputPath dotnet/filtered-unit.slnx
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
|
||||
-TestProjectNameIncludeFilter "*IntegrationTests*" `
|
||||
-TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" `
|
||||
-OutputPath dotnet/filtered-integration.slnx
|
||||
|
||||
- name: Run Unit Tests
|
||||
shell: pwsh
|
||||
working-directory: dotnet
|
||||
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
|
||||
env:
|
||||
# Cosmos DB Emulator connection settings
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
|
||||
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
|
||||
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
|
||||
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
|
||||
fi
|
||||
done
|
||||
|
||||
- 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
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
# This setup action is required for both Durable Task and Azure Functions integration tests.
|
||||
# We only run it on Ubuntu since the Durable Task and Azure Functions features are not available
|
||||
# on .NET Framework (net472) which is what we use the Windows runner for.
|
||||
- name: Set up Durable Task and Azure Functions Integration Test Emulators
|
||||
if: github.event_name != 'pull_request' && matrix.integration-tests && matrix.os == 'ubuntu-latest'
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
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 `
|
||||
--report-junit `
|
||||
--results-directory ../IntegrationTestResults/ `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-not-trait "Category=IntegrationDisabled" `
|
||||
--filter-not-trait "Category=FoundryHostedAgents" `
|
||||
--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 }}
|
||||
# Anthropic Models
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_CHAT_MODEL_NAME: ${{ vars.ANTHROPIC_CHAT_MODEL_NAME }}
|
||||
ANTHROPIC_REASONING_MODEL_NAME: ${{ vars.ANTHROPIC_REASONING_MODEL_NAME }}
|
||||
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@2a82782178b2816d9d6960a7345fdd164791b323 # 5.5.3
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.4.18
|
||||
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
|
||||
path: ./TestResults/Reports # Directory containing files to upload
|
||||
|
||||
- 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
|
||||
|
||||
- name: Upload integration test results
|
||||
if: always() && github.event_name != 'pull_request' && matrix.integration-tests
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: dotnet-test-results-${{ matrix.targetFramework }}-${{ matrix.os }}
|
||||
path: IntegrationTestResults/**/*.junit
|
||||
if-no-files-found: ignore
|
||||
|
||||
# The Foundry hosted-agent IT is costly (it builds a container, pushes to ACR, and provisions
|
||||
# live agents on a separate Foundry project). Running it in its own job keeps the overall
|
||||
# workflow time roughly flat: it executes in parallel to dotnet-build and dotnet-test and is
|
||||
# gated on paths-filter.outputs.foundryHostingChanges so unrelated edits skip the work.
|
||||
dotnet-foundry-hosted-it:
|
||||
needs: paths-filter
|
||||
if: github.event_name != 'pull_request' && needs.paths-filter.outputs.foundryHostingChanges == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
configuration: Release
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
|
||||
- name: Free runner disk space
|
||||
uses: ./.github/actions/free-runner-disk-space
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
# Build the test csproj directly instead of a filtered slnx + -f override.
|
||||
# The test project pins TargetFrameworks=net10.0 and its ProjectReference closure
|
||||
# gives MSBuild a single-rooted graph, so each multi-targeted dependency is invoked
|
||||
# exactly once for net10.0. This avoids the MSB3026/MSB3491/MSB4018/MSB3883 file-lock
|
||||
# collisions caused by parallel inner-builds racing on shared bin/obj output paths
|
||||
# under the previous slnx + global TFM override approach.
|
||||
- name: Build Foundry hosted IT (and its deps)
|
||||
shell: bash
|
||||
run: dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c "$configuration" --warnaserror
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
# We rebuild and push the test container image on every IT run so framework code changes
|
||||
# are picked up; the image tag is content-hashed across the test container source AND its
|
||||
# framework project references, so identical content is a no-op push.
|
||||
#
|
||||
# The script always passes --no-dependencies to dotnet publish so publish never re-touches
|
||||
# the framework lib DLLs the prior "Build Foundry hosted IT (and its deps)" step produced.
|
||||
# This structurally eliminates the MSB3026 collision that VBCSCompiler from the prebuild
|
||||
# would otherwise cause by holding file handles to those DLLs. Do not remove the prebuild
|
||||
# step: the subsequent `dotnet test --no-build` step and the publish's ProjectReference
|
||||
# resolution both depend on the prebuilt outputs being present.
|
||||
- name: Build and push Foundry Hosted Agents test container
|
||||
id: build-foundry-hosted-image
|
||||
shell: pwsh
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
$registry = "${{ vars.IT_HOSTED_AGENT_REGISTRY }}"
|
||||
if ([string]::IsNullOrWhiteSpace($registry)) {
|
||||
throw "IT_HOSTED_AGENT_REGISTRY not set in the integration environment."
|
||||
}
|
||||
& "${{ github.workspace }}/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1" -Registry $registry | Tee-Object -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Run Foundry Hosted Agents Integration Tests
|
||||
shell: pwsh
|
||||
working-directory: dotnet
|
||||
run: |
|
||||
dotnet test --project tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj `
|
||||
-c $env:configuration `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-trait "Category=FoundryHostedAgents"
|
||||
env:
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.IT_HOSTED_AGENT_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.IT_HOSTED_AGENT_MODEL_DEPLOYMENT_NAME }}
|
||||
# Azure AI Search (for the azure-search-rag scenario). Reuses the integration
|
||||
# environment secrets shared with python-sample-validation.yml. The index is
|
||||
# provisioned out of band; see dotnet/tests/Foundry.Hosting.IntegrationTests/README.md
|
||||
# for the required schema and seed content.
|
||||
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
|
||||
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
|
||||
# IT_HOSTED_AGENT_IMAGE was exported into $GITHUB_ENV by the previous step.
|
||||
|
||||
# DurableTask and AzureFunctions integration tests (ubuntu/net10.0 only).
|
||||
# Split from main dotnet-test job for path-based filtering and parallelism.
|
||||
dotnet-test-functions:
|
||||
needs: [paths-filter]
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
(needs.paths-filter.outputs.functionsChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true' ||
|
||||
github.event_name == 'schedule' ||
|
||||
github.event_name == 'workflow_dispatch')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Free runner disk space
|
||||
uses: ./.github/actions/free-runner-disk-space
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
- name: Build functions integration test projects
|
||||
shell: bash
|
||||
working-directory: dotnet
|
||||
run: |
|
||||
dotnet build ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests -c Release -f net10.0 --warnaserror
|
||||
dotnet build ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests -c Release -f net10.0 --warnaserror
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Set up Durable Task and Azure Functions Integration Test Emulators
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
|
||||
- name: Run Functions Integration Tests
|
||||
shell: pwsh
|
||||
working-directory: dotnet
|
||||
run: |
|
||||
# Run DurableTask integration tests
|
||||
dotnet test `
|
||||
--project ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests `
|
||||
-f net10.0 `
|
||||
-c Release `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--report-junit `
|
||||
--results-directory ../IntegrationTestResults/ `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-not-trait "Category=IntegrationDisabled" `
|
||||
--parallel-algorithm aggressive `
|
||||
--max-threads 2.0x
|
||||
|
||||
# Run AzureFunctions integration tests
|
||||
dotnet test `
|
||||
--project ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests `
|
||||
-f net10.0 `
|
||||
-c Release `
|
||||
--no-build -v Normal `
|
||||
--report-xunit-trx `
|
||||
--report-junit `
|
||||
--results-directory ../IntegrationTestResults/ `
|
||||
--ignore-exit-code 8 `
|
||||
--filter-not-trait "Category=IntegrationDisabled" `
|
||||
--parallel-algorithm aggressive `
|
||||
--max-threads 2.0x
|
||||
env:
|
||||
# OpenAI Models
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
|
||||
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
|
||||
# Azure OpenAI Models
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
|
||||
# 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: Upload functions test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: dotnet-test-results-functions-net10.0-ubuntu-latest
|
||||
path: IntegrationTestResults/**/*.junit
|
||||
if-no-files-found: ignore
|
||||
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, dotnet-foundry-hosted-it, dotnet-test-functions]
|
||||
needs: [dotnet-build-and-test]
|
||||
steps:
|
||||
- name: Get Date
|
||||
shell: bash
|
||||
@@ -572,74 +242,13 @@ jobs:
|
||||
- name: Fail workflow if tests failed
|
||||
id: check_tests_failed
|
||||
if: contains(join(needs.*.result, ','), 'failure')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Failed!')
|
||||
|
||||
- name: Fail workflow if tests cancelled
|
||||
id: check_tests_cancelled
|
||||
if: contains(join(needs.*.result, ','), 'cancelled')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Cancelled!')
|
||||
|
||||
# Integration test trend report (aggregates JUnit XML results from dotnet test jobs)
|
||||
dotnet-integration-test-report:
|
||||
name: Integration Test Report
|
||||
if: >
|
||||
always() &&
|
||||
github.event_name != 'pull_request' &&
|
||||
(contains(join(needs.*.result, ','), 'success') ||
|
||||
contains(join(needs.*.result, ','), 'failure'))
|
||||
needs: [dotnet-test, dotnet-test-functions]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.github/actions/python-setup
|
||||
python
|
||||
- name: Set up python and install the project
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: "3.13"
|
||||
os: ${{ runner.os }}
|
||||
- name: Download all test results from current run
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: dotnet-test-results-*
|
||||
path: dotnet-test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/dotnet-integration-report-history.json
|
||||
key: dotnet-integration-report-history-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
dotnet-integration-report-history-
|
||||
- name: Generate trend report
|
||||
run: >
|
||||
uv run python scripts/integration_test_report/aggregate.py
|
||||
../dotnet-test-results/
|
||||
dotnet-integration-report-history.json
|
||||
dotnet-integration-test-report.md
|
||||
- name: Post to Job Summary
|
||||
if: always()
|
||||
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/dotnet-integration-report-history.json
|
||||
key: dotnet-integration-report-history-${{ github.run_id }}
|
||||
- name: Upload trend report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: dotnet-integration-test-report
|
||||
path: |
|
||||
python/dotnet-integration-test-report.md
|
||||
python/dotnet-integration-report-history.json
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { dotnet: "10.0", configuration: Release, os: ubuntu-latest }
|
||||
- { dotnet: "9.0", configuration: Release, os: ubuntu-latest }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: jitterbit/get-changed-files@b17fbb00bdc0c0f63fcf166580804b4d2cdc2a42 # v1
|
||||
uses: jitterbit/get-changed-files@v1
|
||||
continue-on-error: true
|
||||
|
||||
- name: No C# files changed
|
||||
@@ -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,103 +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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # 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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Set up Durable Task and Azure Functions Integration Test Emulators
|
||||
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==
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
declarative-agents
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # 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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: >-
|
||||
issue-triage-${{ github.repository }}-${{
|
||||
((github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'bug'))
|
||||
|| (github.event.action == 'labeled' && github.event.label.name == 'bug'))
|
||||
&& github.event.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
|
||||
if: ${{ (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'bug')) || (github.event.action == 'labeled' && github.event.label.name == 'bug') }}
|
||||
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 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
issue_number="${ISSUE_NUMBER_EVENT}"
|
||||
|
||||
if [[ ! "$issue_number" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Could not determine issue number from event payload." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "issue_number=${issue_number}" >> "$GITHUB_OUTPUT"
|
||||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check issue author team membership
|
||||
id: check
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
path: target-repo
|
||||
|
||||
# Private DevFlow (maf-dashboard) checkout.
|
||||
- name: Checkout DevFlow
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # 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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # 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 }}
|
||||
DEVFLOW_TOKEN: ${{ secrets.DEVFLOW_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.allow_triage != 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Stopping: issue triage preflight did not allow automation."
|
||||
exit 1
|
||||
|
||||
- name: Reproduce reported issue
|
||||
if: ${{ steps.spam.outputs.allow_triage == 'true' }}
|
||||
id: repro
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_COPILOT_TOKEN: ${{ secrets.GH_COPILOT_TOKEN }}
|
||||
# Not seen by the agent prompt; used only to push a paper-trail
|
||||
# branch back to maf-dashboard at run end.
|
||||
DEVFLOW_TOKEN: ${{ secrets.DEVFLOW_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"
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
script: |
|
||||
@@ -45,58 +45,19 @@ jobs:
|
||||
labels.push("triage")
|
||||
}
|
||||
|
||||
// Helper function to extract field value from issue form body
|
||||
// Issue forms format fields as: ### Field Name\n\nValue
|
||||
function getFormFieldValue(body, fieldName) {
|
||||
if (!body) return null
|
||||
const regex = new RegExp(`###\\s*${fieldName}\\s*\\n\\n([^\\n#]+)`, 'i')
|
||||
const match = body.match(regex)
|
||||
return match ? match[1].trim() : null
|
||||
// Check if the body or the title contains the word 'python' (case-insensitive)
|
||||
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
|
||||
// Add the 'python' label to the array
|
||||
labels.push("python")
|
||||
}
|
||||
|
||||
// Check for language from issue form dropdown first
|
||||
const languageField = getFormFieldValue(body, 'Language')
|
||||
let languageLabelAdded = false
|
||||
|
||||
if (languageField) {
|
||||
if (languageField === 'Python') {
|
||||
labels.push("python")
|
||||
languageLabelAdded = true
|
||||
} else if (languageField === '.NET') {
|
||||
labels.push(".NET")
|
||||
languageLabelAdded = true
|
||||
}
|
||||
// 'None / Not Applicable' - don't add any language label
|
||||
}
|
||||
|
||||
// Fallback: Check if the body or the title contains the word 'python' (case-insensitive)
|
||||
// Only if language wasn't already determined from the form field
|
||||
if (!languageLabelAdded) {
|
||||
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
|
||||
// Add the 'python' label to the array
|
||||
labels.push("python")
|
||||
}
|
||||
|
||||
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
|
||||
if ((body != null && body.match(/\.net/i)) || (title != null && title.match(/\.net/i)) ||
|
||||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
|
||||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
|
||||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
|
||||
// Add the '.NET' label to the array
|
||||
labels.push(".NET")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue type from issue form dropdown
|
||||
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
|
||||
if (issueTypeField) {
|
||||
if (issueTypeField === 'Bug') {
|
||||
labels.push("bug")
|
||||
} else if (issueTypeField === 'Feature Request') {
|
||||
labels.push("enhancement")
|
||||
} else if (issueTypeField === 'Question') {
|
||||
labels.push("question")
|
||||
}
|
||||
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
|
||||
if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) ||
|
||||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
|
||||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
|
||||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
|
||||
// Add the '.NET' label to the array
|
||||
labels.push(".NET")
|
||||
}
|
||||
|
||||
// Add the labels to the issue (only if there are labels to add)
|
||||
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
- uses: actions/github-script@v8
|
||||
name: "Issue/PR: update title"
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
name: Limit community pull requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-limit-${{ github.repository }}-${{ github.event.pull_request.user.login }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
MAX_OPEN_PULL_REQUESTS: '10'
|
||||
PR_LIMIT_EXEMPT_LABEL: pr-limit-exempt
|
||||
TOO_MANY_PRS_LABEL: too-many-prs
|
||||
|
||||
jobs:
|
||||
team_check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_team_member: ${{ steps.check.outputs.is_team_member }}
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check PR author team membership
|
||||
id: check
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.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.PR_NUMBER,
|
||||
});
|
||||
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
|
||||
if (isTeamMember) {
|
||||
core.info(`Author ${author} is a team member; skipping open PR limit.`);
|
||||
} else {
|
||||
core.info(`Author ${author} is not a team member; checking open PR limit.`);
|
||||
}
|
||||
|
||||
limit_open_prs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: team_check
|
||||
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Enforce open PR limit
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
script: |
|
||||
const { enforcePrLimit } = require('./.github/scripts/pr_limit_moderation.js');
|
||||
await enforcePrLimit({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
exemptLabelName: process.env.PR_LIMIT_EXEMPT_LABEL,
|
||||
maxOpenPrs: Number.parseInt(process.env.MAX_OPEN_PULL_REQUESTS, 10),
|
||||
labelName: process.env.TOO_MANY_PRS_LABEL,
|
||||
});
|
||||
@@ -19,21 +19,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
# check out the latest version of the code
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install Chrome for Puppeteer
|
||||
run: npx puppeteer browsers install chrome
|
||||
|
||||
# Checks the status of hyperlinks in all files
|
||||
- name: Run linkspector
|
||||
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1
|
||||
uses: umbrelladocs/action-linkspector@v1
|
||||
with:
|
||||
reporter: local
|
||||
filter_mode: nofilter
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Merge Gatekeeper
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main", "feature*"]
|
||||
branches: [ "main", "feature*" ]
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
|
||||
@@ -13,105 +13,19 @@ concurrency:
|
||||
jobs:
|
||||
merge-gatekeeper:
|
||||
runs-on: ubuntu-latest
|
||||
# Restrict permissions of the GITHUB_TOKEN.
|
||||
# Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
|
||||
permissions:
|
||||
checks: read
|
||||
statuses: read
|
||||
steps:
|
||||
- name: Wait for required checks
|
||||
- name: Run Merge Gatekeeper
|
||||
# NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs:
|
||||
# https://github.com/upsidr/merge-gatekeeper/tags
|
||||
# https://github.com/upsidr/merge-gatekeeper/branches
|
||||
uses: upsidr/merge-gatekeeper@v1
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
TIMEOUT_SECONDS: "3600"
|
||||
INTERVAL_SECONDS: "30"
|
||||
SELF_JOB_NAME: ${{ github.job }}
|
||||
# "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_NAMES: "CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results,review"
|
||||
with:
|
||||
script: |
|
||||
const timeoutSeconds = Number(process.env.TIMEOUT_SECONDS);
|
||||
const intervalSeconds = Number(process.env.INTERVAL_SECONDS);
|
||||
const selfName = process.env.SELF_JOB_NAME;
|
||||
const ignored = new Set(
|
||||
process.env.IGNORED_NAMES.split(',').map((s) => s.trim()).filter(Boolean),
|
||||
);
|
||||
|
||||
const sha = context.payload.pull_request.head.sha;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// Mirrors upsidr/merge-gatekeeper: merge combined-statuses and check-runs
|
||||
// for the PR head SHA, with combined-statuses winning on name collision.
|
||||
async function collectChecks() {
|
||||
const merged = new Map();
|
||||
|
||||
const combined = await github.rest.repos.getCombinedStatusForRef({
|
||||
owner, repo, ref: sha, per_page: 100,
|
||||
});
|
||||
for (const s of combined.data.statuses ?? []) {
|
||||
if (!merged.has(s.context)) {
|
||||
// Combined-status states: success | pending | error | failure
|
||||
merged.set(s.context, { name: s.context, state: s.state });
|
||||
}
|
||||
}
|
||||
|
||||
const runs = await github.paginate(github.rest.checks.listForRef, {
|
||||
owner, repo, ref: sha, per_page: 100,
|
||||
});
|
||||
for (const r of runs) {
|
||||
if (merged.has(r.name)) continue;
|
||||
let state;
|
||||
if (r.status !== 'completed') {
|
||||
state = 'pending';
|
||||
} else if (r.conclusion === 'skipped') {
|
||||
continue; // Skipped runs are dropped, matching the original action.
|
||||
} else if (r.conclusion === 'success' || r.conclusion === 'neutral') {
|
||||
state = 'success';
|
||||
} else {
|
||||
// cancelled | timed_out | action_required | stale | failure
|
||||
state = 'error';
|
||||
}
|
||||
merged.set(r.name, { name: r.name, state });
|
||||
}
|
||||
|
||||
return [...merged.values()];
|
||||
}
|
||||
|
||||
function evaluate(entries) {
|
||||
const failed = [];
|
||||
const pending = [];
|
||||
const succeeded = [];
|
||||
for (const e of entries) {
|
||||
if (e.name === selfName || ignored.has(e.name)) continue;
|
||||
if (e.state === 'success') succeeded.push(e.name);
|
||||
else if (e.state === 'error' || e.state === 'failure') failed.push(e.name);
|
||||
else pending.push(e.name);
|
||||
}
|
||||
return { failed, pending, succeeded };
|
||||
}
|
||||
|
||||
const deadline = Date.now() + timeoutSeconds * 1000;
|
||||
for (;;) {
|
||||
const entries = await collectChecks();
|
||||
const { failed, pending, succeeded } = evaluate(entries);
|
||||
|
||||
core.info(
|
||||
`succeeded=${succeeded.length} pending=${pending.length} failed=${failed.length}`,
|
||||
);
|
||||
if (failed.length) {
|
||||
core.setFailed(`Failing checks: ${failed.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
if (pending.length === 0) {
|
||||
core.info(`All required checks passed: ${succeeded.join(', ') || '(none)'}`);
|
||||
return;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
core.setFailed(`Timed out waiting for: ${pending.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
core.info(`Waiting on (${pending.length}): ${pending.slice(0, 10).join(', ')}${pending.length > 10 ? ', …' : ''}`);
|
||||
await sleep(intervalSeconds * 1000);
|
||||
}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout: 3600
|
||||
interval: 30
|
||||
|
||||
@@ -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:
|
||||
@@ -27,9 +27,7 @@ jobs:
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -37,105 +35,15 @@ 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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/prek
|
||||
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
|
||||
@@ -1,610 +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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 }}
|
||||
OLLAMA_MODEL: qwen2.5:1.5b
|
||||
OLLAMA_EMBEDDING_MODEL: nomic-embed-text
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Install Ollama
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
working-directory: .
|
||||
- name: Cache Ollama models
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
- name: Start Ollama and pull models
|
||||
run: |
|
||||
# Stop any Ollama instance auto-started by the install script
|
||||
pkill ollama || true
|
||||
sleep 2
|
||||
ollama serve &
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
# Pull models with retry for transient 429 rate limits
|
||||
for model in qwen2.5:1.5b nomic-embed-text; do
|
||||
pulled=false
|
||||
for attempt in 1 2 3; do
|
||||
if ollama pull "$model"; then
|
||||
pulled=true
|
||||
break
|
||||
fi
|
||||
echo "Retry $attempt for $model (waiting 15s)..."
|
||||
sleep 15
|
||||
done
|
||||
if [ "$pulled" != "true" ]; then
|
||||
echo "ERROR: Failed to pull $model after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
working-directory: .
|
||||
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Set up Azure Functions Integration Test Emulators
|
||||
uses: ./.github/actions/azure-functions-integration-setup
|
||||
id: azure-functions-setup
|
||||
- name: Test with pytest (Functions + Durable Task integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/azurefunctions/tests/integration_tests
|
||||
packages/durabletask/tests/integration_tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
-x
|
||||
--timeout=480 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-functions
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Foundry integration tests
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-cosmos
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# GitHub Copilot integration tests
|
||||
python-tests-github-copilot:
|
||||
name: Python Integration Tests - GitHub Copilot
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
GITHUB_COPILOT_TIMEOUT: "120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Test with pytest (GitHub Copilot integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/github_copilot/tests
|
||||
-m integration
|
||||
--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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-github-copilot
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Integration test trend report (aggregates per-job JUnit XML results)
|
||||
python-integration-test-report:
|
||||
name: Integration 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,
|
||||
python-tests-github-copilot,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-integration-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
integration-report-history-integration-
|
||||
- name: Generate trend report
|
||||
run: >
|
||||
uv run python scripts/integration_test_report/aggregate.py
|
||||
../test-results/
|
||||
integration-report-history.json
|
||||
integration-test-report.md
|
||||
- name: Post to Job Summary
|
||||
if: always()
|
||||
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-integration-${{ github.run_id }}
|
||||
- name: Upload unified trend report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: integration-test-report
|
||||
path: |
|
||||
python/integration-test-report.md
|
||||
python/integration-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-github-copilot
|
||||
]
|
||||
steps:
|
||||
- name: Fail workflow if tests failed
|
||||
if: contains(join(needs.*.result, ','), 'failure')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Failed!')
|
||||
|
||||
- name: Fail workflow if tests cancelled
|
||||
if: contains(join(needs.*.result, ','), 'cancelled')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Cancelled!')
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
outputs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
@@ -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
|
||||
|
||||
@@ -94,7 +90,7 @@ jobs:
|
||||
# Surface failing tests
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
with:
|
||||
path: ./python/packages/lab/**.xml
|
||||
summary: true
|
||||
|
||||
@@ -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,63 +26,15 @@ 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 }}
|
||||
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
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/**'
|
||||
github_copilot:
|
||||
- 'python/packages/github_copilot/**'
|
||||
# run only if 'python' files were changed
|
||||
- name: python tests
|
||||
if: steps.filter.outputs.python == 'true'
|
||||
@@ -96,340 +43,51 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: 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@20b595761ba9bf89e115e875f8bc863f913bc8ad # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: 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@20b595761ba9bf89e115e875f8bc863f913bc8ad # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: 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@20b595761ba9bf89e115e875f8bc863f913bc8ad # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 }}
|
||||
OLLAMA_MODEL: qwen2.5:1.5b
|
||||
OLLAMA_EMBEDDING_MODEL: nomic-embed-text
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Install Ollama
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
working-directory: .
|
||||
- name: Cache Ollama models
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
- name: Start Ollama and pull models
|
||||
run: |
|
||||
# Stop any Ollama instance auto-started by the install script
|
||||
pkill ollama || true
|
||||
sleep 2
|
||||
ollama serve &
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
# Pull models with retry for transient 429 rate limits
|
||||
for model in qwen2.5:1.5b nomic-embed-text; do
|
||||
pulled=false
|
||||
for attempt in 1 2 3; do
|
||||
if ollama pull "$model"; then
|
||||
pulled=true
|
||||
break
|
||||
fi
|
||||
echo "Retry $attempt for $model (waiting 15s)..."
|
||||
sleep 15
|
||||
done
|
||||
if [ "$pulled" != "true" ]; then
|
||||
echo "ERROR: Failed to pull $model after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
working-directory: .
|
||||
- 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@20b595761ba9bf89e115e875f8bc863f913bc8ad # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up python and install the project
|
||||
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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -437,373 +95,104 @@ 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=480 --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 300 --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@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up python and install the project
|
||||
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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
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
|
||||
timeout-minutes: 10
|
||||
run: uv run poe azure-ai-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@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: 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@20b595761ba9bf89e115e875f8bc863f913bc8ad # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: 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@20b595761ba9bf89e115e875f8bc863f913bc8ad # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-cosmos
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# GitHub Copilot integration tests
|
||||
python-tests-github-copilot:
|
||||
name: Python Tests - GitHub Copilot 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.githubCopilotChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
GITHUB_COPILOT_TIMEOUT: "120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Test with pytest (GitHub Copilot integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/github_copilot/tests
|
||||
-m integration
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: GitHub Copilot integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-github-copilot
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Integration test trend report (aggregates per-job JUnit XML results)
|
||||
python-integration-test-report:
|
||||
name: Integration 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,
|
||||
python-tests-github-copilot,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-merge-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
integration-report-history-merge-
|
||||
- name: Generate trend report
|
||||
run: >
|
||||
uv run python scripts/integration_test_report/aggregate.py
|
||||
../test-results/
|
||||
integration-report-history.json
|
||||
integration-test-report.md
|
||||
- name: Post to Job Summary
|
||||
if: always()
|
||||
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-merge-${{ github.run_id }}
|
||||
- name: Upload unified trend report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: integration-test-report
|
||||
path: |
|
||||
python/integration-test-report.md
|
||||
python/integration-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-github-copilot,
|
||||
python-tests-core,
|
||||
python-tests-azure-ai
|
||||
]
|
||||
steps:
|
||||
|
||||
- name: Fail workflow if tests failed
|
||||
id: check_tests_failed
|
||||
if: contains(join(needs.*.result, ','), 'failure')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Failed!')
|
||||
|
||||
- name: Fail workflow if tests cancelled
|
||||
id: check_tests_cancelled
|
||||
if: contains(join(needs.*.result, ','), 'cancelled')
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: core.setFailed('Integration Tests Cancelled!')
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Build the package
|
||||
run: uv run poe --directory packages/${{ env.PACKAGE }} build
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
python/dist/*
|
||||
|
||||
@@ -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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Download all validation reports
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: validation-report-*
|
||||
path: reports/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Restore validation history
|
||||
id: cache-restore
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # 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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
|
||||
- name: Upload trend report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-trend-report
|
||||
path: trend-report.md
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
@@ -20,11 +19,11 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Download coverage report
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
path: ./python
|
||||
merge-multiple: true
|
||||
@@ -35,21 +34,14 @@ jobs:
|
||||
# because the workflow_run event does not have access to the PR number
|
||||
# The PR number is needed to post the comment on the PR
|
||||
run: |
|
||||
if [ ! -s pr_number ]; then
|
||||
echo "PR number file 'pr_number' is missing or empty"
|
||||
exit 1
|
||||
fi
|
||||
PR_NUMBER=$(cat pr_number)
|
||||
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::PR number file contains invalid content"
|
||||
exit 1
|
||||
fi
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
|
||||
echo "PR number: $PR_NUMBER"
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
|
||||
uses: MishaKav/pytest-coverage-comment@v1.1.59
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
pytest-xml-coverage-path: python/python-coverage.xml
|
||||
title: "Python Test Coverage Report"
|
||||
|
||||
@@ -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,9 +18,9 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
UV_PYTHON: "3.10"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
# Save the PR number to a file since the workflow_run event
|
||||
# in the coverage report workflow does not have access to it
|
||||
- name: Save PR number
|
||||
@@ -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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
path: |
|
||||
python/python-coverage.xml
|
||||
|
||||
@@ -27,28 +27,27 @@ jobs:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v5
|
||||
- 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 }}
|
||||
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
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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' }}
|
||||
@@ -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,29 +199,20 @@ temp*/
|
||||
.tmp/
|
||||
.temp/
|
||||
|
||||
agents.md
|
||||
|
||||
# AI
|
||||
**/.checkpoints/
|
||||
.claude/
|
||||
.omc/
|
||||
.omx/
|
||||
WARP.md
|
||||
**/memory-bank/
|
||||
**/projectBrief.md
|
||||
**/tmpclaude*
|
||||
.kiro/
|
||||
# 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
|
||||
@@ -239,15 +224,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
|
||||
|
||||
# Local tool state
|
||||
.omc/
|
||||
.omx/
|
||||
|
||||
**/issues/
|
||||
.test_*
|
||||
|
||||
@@ -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,16 +2,12 @@
|
||||
|
||||
# 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/)
|
||||
[](https://github.com/microsoft/agent-framework/stargazers)
|
||||
|
||||
|
||||
Microsoft Agent Framework (MAF) is an open, multi-language framework for building **production-grade AI agents and multi-agent workflows** in **.NET and Python**.
|
||||
|
||||
Microsoft Agent Framework is built for teams taking agents from prototype to production. It provides a consistent foundation for building, orchestrating, and operating agent systems across Python and .NET, while keeping architecture choices open as requirements evolve, and supports a broad ecosystem including Microsoft Foundry, Azure OpenAI, OpenAI, and the GitHub Copilot SDK, with samples and hosting patterns for both local development and cloud deployment.
|
||||
Welcome to Microsoft's comprehensive multi-language framework for building, orchestrating, and deploying AI agents with support for both .NET and Python implementations. This framework provides everything from simple chat agents to complex multi-agent workflows with graph-based orchestration.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=AAgdMhftj8w" title="Watch the full Agent Framework introduction (30 min)">
|
||||
@@ -25,58 +21,14 @@ Microsoft Agent Framework is built for teams taking agents from prototype to pro
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Is this the right framework for you?
|
||||
## đź“‹ Getting Started
|
||||
|
||||
MAF is a strong fit if you:
|
||||
- are building agents and workflows you expect to run in production,
|
||||
- need orchestration beyond a single prompt or stateless chat loop,
|
||||
- want graph-based patterns such as sequential, concurrent, handoff, and group collaboration,
|
||||
- care about durability, restartability, observability, governance, or human-in-the-loop control,
|
||||
- need provider flexibility so your architecture can evolve without major rewrites.
|
||||
### 📦 Installation
|
||||
|
||||
## Key Features
|
||||
Explore new MAF capabilities and real implementation patterns on the [official blog](https://devblogs.microsoft.com/agent-framework/).
|
||||
|
||||
- **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/)
|
||||
- **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/)
|
||||
- **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/)
|
||||
- **Orchestration Patterns & Workflows**: Build multi-agent systems with graph-based workflows supporting sequential, concurrent, handoff, and group collaboration patterns; includes checkpointing, streaming, human-in-the-loop, and time-travel
|
||||
- [Python workflows](./python/samples/03-workflows/) | [.NET workflows](./dotnet/samples/03-workflows/)
|
||||
- **Foundry Hosted Agents (new)**: Deploy and host your agents to Foundry-hosted infrastructure with just 2 additional lines of code
|
||||
- [Python samples](./python/samples/04-hosting/foundry-hosted-agents/) | [.NET samples](./dotnet/samples/04-hosting/FoundryHostedAgents/)
|
||||
- **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/)
|
||||
- **Declarative Agents**: Define agents using YAML for faster setup and versioning
|
||||
- [Declarative agent samples](./declarative-agents/)
|
||||
- **Agent Skills**: Build domain-specific knowledge bases from multiple sources—files, inline code, class libraries—for agents to discover and use
|
||||
- [Skills design](./docs/decisions/0021-agent-skills-design.md)
|
||||
- **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
|
||||
- [See the DevUI in action](https://www.youtube.com/watch?v=mOAaGY4WPvc)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Learning Resources](#learning-resources)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Basic Agent - Python](#basic-agent---python)
|
||||
- [Basic Agent - .NET](#basic-agent---net)
|
||||
- [More Examples & Samples](#more-examples--samples)
|
||||
- [Community & Feedback](#community--feedback)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Contributor Resources](#contributor-resources)
|
||||
|
||||
## Getting Started
|
||||
### Installation
|
||||
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.
|
||||
```
|
||||
@@ -85,13 +37,9 @@ pip install agent-framework
|
||||
|
||||
```bash
|
||||
dotnet add package Microsoft.Agents.AI
|
||||
# For Foundry integration (used in the .NET quickstart below):
|
||||
dotnet add package Microsoft.Agents.AI.Foundry
|
||||
dotnet add package Azure.AI.Projects
|
||||
dotnet add package Azure.Identity
|
||||
```
|
||||
|
||||
### Learning Resources
|
||||
### 📚 Documentation
|
||||
|
||||
- **[Overview](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview)** - High level overview of the framework
|
||||
- **[Quick Start](https://learn.microsoft.com/agent-framework/tutorials/quick-start)** - Get started with a simple agent
|
||||
@@ -100,34 +48,67 @@ dotnet add package Azure.Identity
|
||||
- **[Migration from Semantic Kernel](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel)** - Guide to migrate from Semantic Kernel
|
||||
- **[Migration from AutoGen](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-autogen)** - Guide to migrate from AutoGen
|
||||
|
||||
### Quickstart
|
||||
### ✨ **Highlights**
|
||||
|
||||
#### Basic Agent - Python
|
||||
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, human-in-the-loop, and time-travel capabilities
|
||||
- [Python workflows](./python/samples/getting_started/workflows/) | [.NET workflows](./dotnet/samples/GettingStarted/Workflows/)
|
||||
- **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
|
||||
- [DevUI package](./python/packages/devui/)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=mOAaGY4WPvc">
|
||||
<img src="https://img.youtube.com/vi/mOAaGY4WPvc/hqdefault.jpg" alt="See the DevUI in action" width="480">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=mOAaGY4WPvc">
|
||||
See the DevUI in action (1 min)
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
|
||||
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
|
||||
- **Observability**: Built-in OpenTelemetry integration for distributed tracing, monitoring, and debugging
|
||||
- [Python observability](./python/samples/getting_started/observability/) | [.NET telemetry](./dotnet/samples/GettingStarted/AgentOpenTelemetry/)
|
||||
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
|
||||
- [Python examples](./python/samples/getting_started/agents/) | [.NET examples](./dotnet/samples/GettingStarted/AgentProviders/)
|
||||
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
|
||||
- [Python middleware](./python/samples/getting_started/middleware/) | [.NET middleware](./dotnet/samples/GettingStarted/Agents/Agent_Step14_Middleware/)
|
||||
|
||||
### đź’¬ **We want your feedback!**
|
||||
|
||||
- For bugs, please file a [GitHub issue](https://github.com/microsoft/agent-framework/issues).
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Basic Agent - Python
|
||||
|
||||
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="HaikuAgent",
|
||||
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."))
|
||||
@@ -136,24 +117,39 @@ if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
#### Basic Agent - .NET
|
||||
Create a simple Agent, using Microsoft Foundry that writes a haiku about the Microsoft Agent Framework
|
||||
### Basic Agent - .NET
|
||||
|
||||
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// This sample shows how to create and run a basic agent with AIProjectClient.AsAIAgent(...).
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
using System;
|
||||
using OpenAI;
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
AIAgent agent =
|
||||
new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, instructions: "You are an upbeat assistant that writes beautifully.", name: "HaikuAgent");
|
||||
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.");
|
||||
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
@@ -161,40 +157,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
|
||||
|
||||
## Community & Feedback
|
||||
|
||||
- **Found a bug?** File a [GitHub issue](https://github.com/microsoft/agent-framework/issues) to help us improve.
|
||||
- **Enjoying MAF?** [](https://github.com/microsoft/agent-framework) to show your support and help others discover the project.
|
||||
- **Have questions?** Join our [Discord](https://discord.gg/b5zjErwbQM) or visit [weekly office hours](./COMMUNITY.md#public-community-office-hours).
|
||||
|
||||
## 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
|
||||
For environment variable configuration specific to each sample, refer to the README in the sample directory ([Python samples](./python/samples/) | [.NET samples](./dotnet/samples/)).
|
||||
- [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
|
||||
|
||||
@@ -205,9 +176,4 @@ For environment variable configuration specific to each sample, refer to the REA
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# Support
|
||||
|
||||
## How to file issues and get help
|
||||
|
||||
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
|
||||
issues before filing new issues to avoid duplicates. For new issues, file your bug or
|
||||
feature request as a new Issue.
|
||||
|
||||
For help and questions about using this project, please create a GitHub issue.
|
||||
|
||||
AI Support team will support Microsoft Agent Framework issues for customers under a **Unified support agreement when the issue arises from usage of Azure AI services** (Foundry Models, Foundry Agents etc.) in conjunction with the SDK. Conversely, if customer has any other / non unified support agreement and/or Agent Framework SDK is used in a way **not involving an Azure service**, it is treated as a purely open-source tool – Microsoft’s support organization will not handle it, and users should use GitHub or forums for assistance
|
||||
|
||||
For Copilot Studio SDK implementation issues, customers should use GitHub Issues for assistance, as outlined above. Conversely, for prerequisites managed within the Copilot Studio portal, customers can rely on the standard Microsoft Copilot Studio support channels.
|
||||
|
||||
## Microsoft Support Policy
|
||||
|
||||
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
|
||||
# Support
|
||||
|
||||
## How to file issues and get help
|
||||
|
||||
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
|
||||
issues before filing new issues to avoid duplicates. For new issues, file your bug or
|
||||
feature request as a new Issue.
|
||||
|
||||
For help and questions about using this project, please create a GitHub issue.
|
||||
|
||||
AI Support team will support Microsoft Agent Framework issues for customers under a **Unified support agreement when the issue arises from usage of Azure AI services** (Foundry Models, Foundry Agents etc.) in conjunction with the SDK. Conversely, if customer has any other / non unified support agreement and/or Agent Framework SDK is used in a way **not involving an Azure service**, it is treated as a purely open-source tool – Microsoft’s support organization will not handle it, and users should use GitHub or forums for assistance
|
||||
|
||||
For Copilot Studio SDK implementation issues, customers should use GitHub Issues for assistance, as outlined above. Conversely, for prerequisites managed within the Copilot Studio portal, customers can rely on the standard Microsoft Copilot Studio support channels.
|
||||
|
||||
## Microsoft Support Policy
|
||||
|
||||
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
|
||||
|
||||
@@ -42,9 +42,9 @@ Microsoft Agent Framework relies on existing LLMs. Using the framework retains c
|
||||
|
||||
**Framework-Specific Limitations**:
|
||||
|
||||
- **Platform Requirements**: Python 3.10+ required, specific .NET versions (.NET 8.0, 9.0, 10.0, netstandard2.0, net472)
|
||||
- **Platform Requirements**: Python 3.10+ required, specific .NET versions (.NET 8.0, 9.0, netstandard2.0, net472)
|
||||
- **API Dependencies**: Requires proper configuration of LLM provider keys and endpoints
|
||||
- **Orchestration Features**: Advanced orchestration patterns including GroupChat, Sequential, and Concurrent workflows are now available in both Python and .NET implementations. See the respective language documentation for examples.
|
||||
- **Orchestration Features**: Advanced orchestration patterns like GroupChat, Sequential, and Concurrent orchestrations are "coming soon" for Python implementation
|
||||
- **Privacy and Data Protection**: The framework allows for human participation in conversations between agents. It is important to ensure that user data and conversations are protected and that developers use appropriate measures to safeguard privacy.
|
||||
- **Accountability and Transparency**: The framework involves multiple agents conversing and collaborating, it is important to establish clear accountability and transparency mechanisms. Users should be able to understand and trace the decision-making process of the agents involved in order to ensure accountability and address any potential issues or biases.
|
||||
- **Security & unintended consequences**: The use of multi-agent conversations and automation in complex tasks may have unintended consequences. Especially, allowing agents to make changes in external environments through tool calls or function execution could pose significant risks. Developers should carefully consider the potential risks and ensure that appropriate safeguards are in place to prevent harm or negative outcomes, including keeping a human in the loop for decision making.
|
||||
|
||||
@@ -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,25 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
provider: AzureOpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,25 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Assistants
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,25 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,25 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
model:
|
||||
id: gpt-4o-mini
|
||||
provider: AzureOpenAI
|
||||
apiType: Responses
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,18 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -1,29 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions using the tools provided.
|
||||
model:
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
allowMultipleToolCalls: true
|
||||
chatToolMode: auto
|
||||
tools:
|
||||
- kind: function
|
||||
name: GetWeather
|
||||
description: Get the weather for a given location.
|
||||
bindings:
|
||||
get_weather: get_weather
|
||||
parameters:
|
||||
properties:
|
||||
location:
|
||||
kind: string
|
||||
description: The city and state, e.g. San Francisco, CA
|
||||
required: true
|
||||
unit:
|
||||
kind: string
|
||||
description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
|
||||
required: false
|
||||
enum:
|
||||
- celsius
|
||||
- fahrenheit
|
||||
@@ -1,22 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: Remote
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -1,21 +0,0 @@
|
||||
kind: Prompt
|
||||
name: MicrosoftLearnAgent
|
||||
description: Microsoft Learn Agent
|
||||
instructions: You answer questions by searching the Microsoft Learn content only.
|
||||
model:
|
||||
id: =Env.FOUNDRY_MODEL
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: remote
|
||||
endpoint: =Env.FOUNDRY_PROJECT_ENDPOINT
|
||||
tools:
|
||||
- kind: mcp
|
||||
name: microsoft_learn
|
||||
description: Get information from Microsoft Learn.
|
||||
url: https://learn.microsoft.com/api/mcp
|
||||
approvalMode:
|
||||
kind: never
|
||||
allowedTools:
|
||||
- microsoft_docs_search
|
||||
@@ -1,22 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
|
||||
model:
|
||||
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: remote
|
||||
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
@@ -1,28 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: =Env.OPENAI_MODEL
|
||||
provider: OpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: key
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,28 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Assistants
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,28 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Chat
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: ApiKey
|
||||
key: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
type: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
type: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,28 +0,0 @@
|
||||
kind: Prompt
|
||||
name: Assistant
|
||||
description: Helpful assistant
|
||||
instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
|
||||
model:
|
||||
id: gpt-4.1-mini
|
||||
provider: OpenAI
|
||||
apiType: Responses
|
||||
options:
|
||||
temperature: 0.9
|
||||
topP: 0.95
|
||||
connection:
|
||||
kind: key
|
||||
apiKey: =Env.OPENAI_API_KEY
|
||||
outputSchema:
|
||||
properties:
|
||||
language:
|
||||
kind: string
|
||||
required: true
|
||||
description: The language of the answer.
|
||||
answer:
|
||||
kind: string
|
||||
required: true
|
||||
description: The answer text.
|
||||
type:
|
||||
kind: string
|
||||
required: true
|
||||
description: The type of the response.
|
||||
@@ -1,164 +0,0 @@
|
||||
#
|
||||
# This workflow demonstrates using multiple agents to provide automated
|
||||
# troubleshooting steps to resolve common issues with escalation options.
|
||||
#
|
||||
# Example input:
|
||||
# My PC keeps rebooting and I can't use it.
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
# Interact with user until the issue has been resolved or
|
||||
# a determination is made that a ticket is required.
|
||||
- kind: InvokeAzureAgent
|
||||
id: service_agent
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: SelfServiceAgent
|
||||
input:
|
||||
externalLoop:
|
||||
when: |-
|
||||
=Not(Local.ServiceParameters.IsResolved)
|
||||
And
|
||||
Not(Local.ServiceParameters.NeedsTicket)
|
||||
output:
|
||||
responseObject: Local.ServiceParameters
|
||||
|
||||
# All done if issue is resolved.
|
||||
- kind: ConditionGroup
|
||||
id: check_if_resolved
|
||||
conditions:
|
||||
|
||||
- condition: =Local.ServiceParameters.IsResolved
|
||||
id: test_if_resolved
|
||||
actions:
|
||||
- kind: GotoAction
|
||||
id: end_when_resolved
|
||||
actionId: all_done
|
||||
|
||||
# Create the ticket.
|
||||
- kind: InvokeAzureAgent
|
||||
id: ticket_agent
|
||||
agent:
|
||||
name: TicketingAgent
|
||||
input:
|
||||
arguments:
|
||||
IssueDescription: =Local.ServiceParameters.IssueDescription
|
||||
AttemptedResolutionSteps: =Local.ServiceParameters.AttemptedResolutionSteps
|
||||
output:
|
||||
responseObject: Local.TicketParameters
|
||||
|
||||
# Capture the attempted resolution steps.
|
||||
- kind: SetVariable
|
||||
id: capture_attempted_resolution
|
||||
variable: Local.ResolutionSteps
|
||||
value: =Local.ServiceParameters.AttemptedResolutionSteps
|
||||
|
||||
# Notify user of ticket identifier.
|
||||
- kind: SendActivity
|
||||
id: log_ticket
|
||||
activity: "Created ticket #{Local.TicketParameters.TicketId}"
|
||||
|
||||
# Determine which team for which route the ticket.
|
||||
- kind: InvokeAzureAgent
|
||||
id: routing_agent
|
||||
agent:
|
||||
name: TicketRoutingAgent
|
||||
input:
|
||||
messages: =UserMessage(Local.ServiceParameters.IssueDescription)
|
||||
output:
|
||||
responseObject: Local.RoutingParameters
|
||||
|
||||
# Notify user of routing decision.
|
||||
- kind: SendActivity
|
||||
id: log_route
|
||||
activity: Routing to {Local.RoutingParameters.TeamName}
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: check_routing
|
||||
conditions:
|
||||
|
||||
- condition: =Local.RoutingParameters.TeamName = "Windows Support"
|
||||
id: route_to_support
|
||||
actions:
|
||||
|
||||
# Invoke the support agent to attempt to resolve the issue.
|
||||
- kind: CreateConversation
|
||||
id: conversation_support
|
||||
conversationId: Local.SupportConversationId
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: support_agent
|
||||
conversationId: =Local.SupportConversationId
|
||||
agent:
|
||||
name: WindowsSupportAgent
|
||||
input:
|
||||
arguments:
|
||||
IssueDescription: =Local.ServiceParameters.IssueDescription
|
||||
AttemptedResolutionSteps: =Local.ServiceParameters.AttemptedResolutionSteps
|
||||
externalLoop:
|
||||
when: |-
|
||||
=Not(Local.SupportParameters.IsResolved)
|
||||
And
|
||||
Not(Local.SupportParameters.NeedsEscalation)
|
||||
output:
|
||||
autoSend: true
|
||||
responseObject: Local.SupportParameters
|
||||
|
||||
# Capture the attempted resolution steps.
|
||||
- kind: SetVariable
|
||||
id: capture_support_resolution
|
||||
variable: Local.ResolutionSteps
|
||||
value: =Local.SupportParameters.ResolutionSummary
|
||||
|
||||
# Check if the issue was resolved by support.
|
||||
- kind: ConditionGroup
|
||||
id: check_resolved
|
||||
conditions:
|
||||
|
||||
# Resolve ticket
|
||||
- condition: =Local.SupportParameters.IsResolved
|
||||
id: handle_if_resolved
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: resolution_agent
|
||||
agent:
|
||||
name: TicketResolutionAgent
|
||||
input:
|
||||
arguments:
|
||||
TicketId: =Local.TicketParameters.TicketId
|
||||
ResolutionSummary: =Local.SupportParameters.ResolutionSummary
|
||||
|
||||
- kind: GotoAction
|
||||
id: end_when_solved
|
||||
actionId: all_done
|
||||
|
||||
# Escalate the ticket by sending an email notification.
|
||||
- kind: CreateConversation
|
||||
id: conversation_escalate
|
||||
conversationId: Local.EscalationConversationId
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: escalate_agent
|
||||
conversationId: =Local.EscalationConversationId
|
||||
agent:
|
||||
name: TicketEscalationAgent
|
||||
input:
|
||||
arguments:
|
||||
TicketId: =Local.TicketParameters.TicketId
|
||||
IssueDescription: =Local.ServiceParameters.IssueDescription
|
||||
ResolutionSummary: =Local.ResolutionSteps
|
||||
externalLoop:
|
||||
when: =Not(Local.EscalationParameters.IsComplete)
|
||||
output:
|
||||
autoSend: true
|
||||
responseObject: Local.EscalationParameters
|
||||
|
||||
# All done
|
||||
- kind: EndWorkflow
|
||||
id: all_done
|
||||
@@ -1,381 +0,0 @@
|
||||
#
|
||||
# This workflow coordinates multiple agents in order to address complex user requests
|
||||
# according to the "Magentic" orchestration pattern introduced by AutoGen.
|
||||
#
|
||||
# For this workflow, several agents used, each with specific roles.
|
||||
#
|
||||
# The following agents are responsible for overseeing and coordinating the workflow:
|
||||
# - Research Agent: Analyze the current task and correlate relevant facts.
|
||||
# - Planner Agent: Analyze the current task and devise an overall plan.
|
||||
# - Manager Agent: Evaluates status and delegate tasks to other agents.
|
||||
# - Summary Agent: Evaluates status and delegate tasks to other agents.
|
||||
#
|
||||
# The following agents have capabilities that are utilized to address the input task:
|
||||
# - Knowledge Agent: Performs generic web searches.
|
||||
# - Coder Agent: Able to write and execute code.
|
||||
# - Weather Agent: Provides weather information.
|
||||
#
|
||||
kind: Workflow
|
||||
maxTurns: 500
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_aASlmF
|
||||
displayName: List all available agents for this orchestrator
|
||||
variable: Local.AvailableAgents
|
||||
value: |-
|
||||
=[
|
||||
{
|
||||
name: "WeatherAgent",
|
||||
description: "Able to retrieve weather information"
|
||||
},
|
||||
{
|
||||
name: "CoderAgent",
|
||||
description: "Able to write and execute Python code"
|
||||
},
|
||||
{
|
||||
name: "KnowledgeAgent",
|
||||
description: "Able to perform generic websearches"
|
||||
}
|
||||
]
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_V6yEbo
|
||||
displayName: Get a summary of all the agents for use in prompts
|
||||
variable: Local.TeamDescription
|
||||
value: "=Concat(ForAll(Local.AvailableAgents, $\"- \" & name & $\": \" & description), Value, \"\n\")"
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_NZ2u0l
|
||||
displayName: Set Task
|
||||
variable: Local.InputTask
|
||||
value: =System.LastMessage.Text
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_10u2ZN
|
||||
displayName: Set Task
|
||||
variable: Local.SeedTask
|
||||
value: =UserMessage(Local.InputTask)
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_yFsbRy
|
||||
activity: Analyzing facts...
|
||||
|
||||
- kind: CreateConversation
|
||||
id: conversation_1a2b3c
|
||||
conversationId: Local.StatusConversationId
|
||||
|
||||
- kind: CreateConversation
|
||||
id: conversation_1x2y3z
|
||||
conversationId: Local.TaskConversationId
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_UDoMUw
|
||||
displayName: Get Facts
|
||||
conversationId: =Local.StatusConversationId
|
||||
agent:
|
||||
name: ResearchAgent
|
||||
output:
|
||||
messages: Local.TaskFacts
|
||||
input:
|
||||
messages: =UserMessage(Local.InputTask)
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_yFsbRz
|
||||
activity: Creating a plan...
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_DsBaJU
|
||||
displayName: Create a Plan
|
||||
conversationId: =Local.StatusConversationId
|
||||
agent:
|
||||
name: PlannerAgent
|
||||
input:
|
||||
arguments:
|
||||
team: =Local.TeamDescription
|
||||
output:
|
||||
messages: Local.Plan
|
||||
|
||||
- kind: SetTextVariable
|
||||
id: setVariable_Kk2LDL
|
||||
displayName: Define instructions
|
||||
variable: Local.TaskInstructions
|
||||
value: |-
|
||||
# TASK
|
||||
Address the following user request:
|
||||
|
||||
{Local.InputTask}
|
||||
|
||||
|
||||
# TEAM
|
||||
Use the following team to answer this request:
|
||||
|
||||
{Local.TeamDescription}
|
||||
|
||||
|
||||
# FACTS
|
||||
Consider this initial fact sheet:
|
||||
|
||||
{MessageText(Local.TaskFacts)}
|
||||
|
||||
|
||||
# PLAN
|
||||
Here is the plan to follow as best as possible:
|
||||
|
||||
{MessageText(Local.Plan)}
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_bwNZiM
|
||||
activity: {Local.TaskInstructions}
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_o3BQkf
|
||||
displayName: Progress Ledger Prompt
|
||||
conversationId: =Local.StatusConversationId
|
||||
agent:
|
||||
name: ManagerAgent
|
||||
input:
|
||||
messages: =UserMessage(Local.AgentResponseText)
|
||||
output:
|
||||
responseObject: Local.ProgressLedger
|
||||
autoSend: false
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_mVIecC
|
||||
conditions:
|
||||
- id: conditionItem_fj432c
|
||||
condition: =Local.ProgressLedger.is_request_satisfied.answer
|
||||
displayName: If Done
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_kdl3mC
|
||||
activity: Completed! {Local.ProgressLedger.is_request_satisfied.reason}
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_Ke3l1d
|
||||
displayName: Generate Response
|
||||
conversationId: =Local.TaskConversationId
|
||||
agent:
|
||||
name: SummaryAgent
|
||||
output:
|
||||
autoSend: true
|
||||
messages: Local.FinalResponse
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_SVoNSV
|
||||
|
||||
- id: conditionItem_yiqund
|
||||
condition: =Local.ProgressLedger.is_in_loop.answer || Not(Local.ProgressLedger.is_progress_being_made.answer)
|
||||
displayName: If Stalling
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_H5lXdD
|
||||
displayName: Increase stall count
|
||||
variable: Local.StallCount
|
||||
value: =Local.StallCount + 1
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_vBTQd3
|
||||
conditions:
|
||||
|
||||
- id: conditionItem_fpaNL9
|
||||
condition: =Local.ProgressLedger.is_in_loop.answer
|
||||
displayName: Is Loop
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_fpaNL9
|
||||
activity: {Local.ProgressLedger.is_in_loop.reason}
|
||||
|
||||
- id: conditionItem_NnqvXh
|
||||
condition: =Not(Local.ProgressLedger.is_progress_being_made.answer)
|
||||
displayName: Is No Progress
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_NnqvXh
|
||||
activity: {Local.ProgressLedger.is_progress_being_made.reason}
|
||||
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_xzNrdM
|
||||
conditions:
|
||||
- id: conditionItem_NlQTBv
|
||||
condition: =Local.StallCount > 2
|
||||
displayName: Stall Count Exceeded
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_H5lXdD
|
||||
activity: Unable to make sufficient progress...
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_4s1Z27
|
||||
conditions:
|
||||
- id: conditionItem_EXAlhZ
|
||||
condition: =Local.RestartCount > 2
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_xKxFUU
|
||||
activity: Stopping after attempting {Local.RestartCount} restarts...
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_GHVrFh
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_cwNZiM
|
||||
activity: Re-analyzing facts...
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_wFJ123
|
||||
displayName: Get New Facts Prompt
|
||||
conversationId: =Local.StatusConversationId
|
||||
agent:
|
||||
name: ResearchAgent
|
||||
output:
|
||||
messages: Local.TaskFacts
|
||||
input:
|
||||
messages: |-
|
||||
=UserMessage(
|
||||
"It's clear we aren't making as much progress as we would like, but we may have learned something new.
|
||||
Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful.
|
||||
Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc.
|
||||
Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited.
|
||||
This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning.
|
||||
|
||||
Here is the old fact sheet:
|
||||
|
||||
{MessageText(Local.TaskFacts)}"
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_dsBaJU
|
||||
activity: Re-analyzing plan...
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_uEJ456
|
||||
displayName: Create new Plan Prompt
|
||||
conversationId: =Local.StatusConversationId
|
||||
agent:
|
||||
name: PlannerAgent
|
||||
output:
|
||||
messages: Local.Plan
|
||||
input:
|
||||
messages: |-
|
||||
=UserMessage(
|
||||
"Please briefly explain what went wrong on this last run (the root cause of the failure),
|
||||
and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
|
||||
As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
|
||||
(do not involve any other outside people since we cannot contact anyone else):
|
||||
|
||||
{Local.TeamDescription}")
|
||||
|
||||
- kind: SetTextVariable
|
||||
id: setVariable_jW7tmM
|
||||
displayName: Set Plan as Context
|
||||
variable: Local.TaskInstructions
|
||||
value: |-
|
||||
# TASK
|
||||
Address the following user request:
|
||||
|
||||
{Local.InputTask}
|
||||
|
||||
|
||||
# TEAM
|
||||
Use the following team to answer this request:
|
||||
|
||||
{Local.TeamDescription}
|
||||
|
||||
|
||||
# FACTS
|
||||
Consider this initial fact sheet:
|
||||
|
||||
{MessageText(Local.TaskFacts)}
|
||||
|
||||
|
||||
# PLAN
|
||||
Here is the plan to follow as best as possible:
|
||||
|
||||
{MessageText(Local.Plan)}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_6J2snP
|
||||
displayName: Reset Stall count
|
||||
variable: Local.StallCount
|
||||
value: 0
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_S6HCgh
|
||||
displayName: Increase Restart count
|
||||
variable: Local.RestartCount
|
||||
value: =Local.RestartCount + 1
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_LzfJ8u
|
||||
actionId: question_o3BQkf
|
||||
|
||||
elseActions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_L7ooQO
|
||||
activity: |-
|
||||
({Local.ProgressLedger.next_speaker.reason})
|
||||
|
||||
{Local.ProgressLedger.next_speaker.answer} - {Local.ProgressLedger.instruction_or_question.answer}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_nxN1mE
|
||||
variable: Local.NextSpeaker
|
||||
value: =Search(Local.AvailableAgents, Local.ProgressLedger.next_speaker.answer, name)
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_QFPiF5
|
||||
conditions:
|
||||
- id: conditionItem_GmigcU
|
||||
condition: =CountRows(Local.NextSpeaker) = 1
|
||||
displayName: If next Agent tool Exists
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_L7ooQO
|
||||
variable: Local.StallCount
|
||||
value: 0
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_orsBf06
|
||||
displayName: Progress Ledger Prompt
|
||||
conversationId: =Local.TaskConversationId
|
||||
agent:
|
||||
name: =First(Local.NextSpeaker).name
|
||||
output:
|
||||
autoSend: true
|
||||
messages: Local.AgentResponse
|
||||
input:
|
||||
messages: =UserMessage(Local.ProgressLedger.instruction_or_question.answer)
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_XzNrdM
|
||||
variable: Local.AgentResponseText
|
||||
value: =MessageText(Local.AgentResponse)
|
||||
|
||||
- kind: ResetVariable
|
||||
id: setVariable_8eIx2A
|
||||
displayName: Clear seed task
|
||||
variable: Local.SeedTask
|
||||
|
||||
elseActions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_BhcsI7
|
||||
activity: Unable to choose next agent...
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_BhcsI7
|
||||
displayName: Increase stall count
|
||||
variable: Local.StallCount
|
||||
value: =Local.StallCount + 1
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_76Hne8
|
||||
actionId: question_o3BQkf
|
||||
@@ -1,30 +0,0 @@
|
||||
#
|
||||
# This workflow demonstrates sequential agent interaction to develop product marketing copy.
|
||||
#
|
||||
# Example input:
|
||||
# An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours.
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_analyst
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: AnalystAgent
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_writer
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: WriterAgent
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_editor
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: EditorAgent
|
||||
@@ -1,58 +0,0 @@
|
||||
#
|
||||
# This workflow demonstrates conversation between two agents: a student and a teacher.
|
||||
# The student attempts to solve the input problem and the teacher provides guidance.
|
||||
#
|
||||
# Example input:
|
||||
# How would you compute the value of PI?
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_student
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: StudentAgent
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_teacher
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: TeacherAgent
|
||||
output:
|
||||
messages: Local.TeacherResponse
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_count_increment
|
||||
variable: Local.TurnCount
|
||||
value: =Local.TurnCount + 1
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: =!IsBlank(Find("CONGRATULATIONS", Upper(MessageText(Local.TeacherResponse))))
|
||||
id: check_turn_done
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_done
|
||||
activity: GOLD STAR!
|
||||
|
||||
- condition: =Local.TurnCount < 4
|
||||
id: check_turn_count
|
||||
actions:
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_student_agent
|
||||
actionId: question_student
|
||||
|
||||
elseActions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_tired
|
||||
activity: Let's try again later...
|
||||
@@ -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.
|
||||
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,55 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" transform="matrix(-1 0 0 1 256 0)" fill="white"/>
|
||||
<path d="M163.006 36.0262C155.129 31.4785 145.424 31.4759 137.533 36.0284L104.002 55.3877C107.838 53.2763 112.07 52.2274 116.296 52.2248C120.633 52.2231 124.976 53.3247 128.871 55.5411L174.091 81.6479C175.11 82.2362 175.738 83.3236 175.738 84.5004V151.263C175.738 152.716 177.313 153.621 178.568 152.895L196.967 142.28C204.845 137.732 209.704 129.326 209.704 120.221V77.7299C209.704 68.6342 204.855 60.227 196.967 55.6697L190.983 52.2141C190.65 52.0008 190.313 51.7921 189.969 51.5933L163.006 36.0262Z" fill="url(#paint0_linear_481_4810)"/>
|
||||
<path d="M163.006 36.0262C155.129 31.4785 145.424 31.4759 137.533 36.0284L104.002 55.3877C107.838 53.2763 112.07 52.2274 116.296 52.2248C120.633 52.2231 124.976 53.3247 128.871 55.5411L174.091 81.6479C175.11 82.2362 175.738 83.3236 175.738 84.5004V151.263C175.738 152.716 177.313 153.621 178.568 152.895L196.967 142.28C204.845 137.732 209.704 129.326 209.704 120.221V77.7299C209.704 68.6342 204.855 60.227 196.967 55.6697L190.983 52.2141C190.65 52.0008 190.313 51.7921 189.969 51.5933L163.006 36.0262Z" fill="url(#paint1_linear_481_4810)"/>
|
||||
<path d="M103.548 55.6397L103.557 55.6451L104.002 55.3877C103.851 55.471 103.698 55.5531 103.548 55.6397Z" fill="url(#paint2_linear_481_4810)"/>
|
||||
<path d="M103.548 55.6397L103.557 55.6451L104.002 55.3877C103.851 55.471 103.698 55.5531 103.548 55.6397Z" fill="url(#paint3_linear_481_4810)"/>
|
||||
<path d="M116.308 52.2209C111.903 52.2271 107.507 53.3498 103.561 55.6366C95.6702 60.1891 90.8239 68.6019 90.8231 77.6986L90.8223 167.846C90.8222 169.786 92.871 171.041 94.599 170.16L103.523 165.611C116.573 158.959 124.788 145.549 124.788 130.902V62.9894C124.778 58.6476 129.49 55.9209 133.25 58.0698L128.879 55.5453C124.976 53.3242 120.645 52.2192 116.308 52.2209Z" fill="url(#paint4_linear_481_4810)"/>
|
||||
<path d="M95.3068 221.682C103.184 226.23 112.889 226.232 120.78 221.68L154.311 202.32C150.475 204.432 146.243 205.481 142.018 205.483C137.68 205.485 133.337 204.383 129.442 202.167L84.2226 176.06C83.2035 175.472 82.5757 174.384 82.5757 173.208L82.5757 106.445C82.5757 104.992 81 104.087 79.7451 104.813L61.3465 115.428C53.4682 119.976 48.6091 128.382 48.6089 137.487L48.6089 179.978C48.6089 189.074 53.4585 197.481 61.3465 202.038L67.3303 205.494C67.6629 205.707 68.0002 205.916 68.3446 206.115L95.3068 221.682Z" fill="url(#paint5_linear_481_4810)"/>
|
||||
<path d="M95.3068 221.682C103.184 226.23 112.889 226.232 120.78 221.68L154.311 202.32C150.475 204.432 146.243 205.481 142.018 205.483C137.68 205.485 133.337 204.383 129.442 202.167L84.2226 176.06C83.2035 175.472 82.5757 174.384 82.5757 173.208L82.5757 106.445C82.5757 104.992 81 104.087 79.7451 104.813L61.3465 115.428C53.4682 119.976 48.6091 128.382 48.6089 137.487L48.6089 179.978C48.6089 189.074 53.4585 197.481 61.3465 202.038L67.3303 205.494C67.6629 205.707 68.0002 205.916 68.3446 206.115L95.3068 221.682Z" fill="url(#paint6_linear_481_4810)"/>
|
||||
<path d="M154.765 202.068L154.756 202.063L154.311 202.32C154.463 202.237 154.615 202.155 154.765 202.068Z" fill="url(#paint7_linear_481_4810)"/>
|
||||
<path d="M154.765 202.068L154.756 202.063L154.311 202.32C154.463 202.237 154.615 202.155 154.765 202.068Z" fill="url(#paint8_linear_481_4810)"/>
|
||||
<path d="M142.003 205.487C146.408 205.481 150.805 204.358 154.751 202.071C162.641 197.519 167.488 189.106 167.488 180.009L167.489 89.8618C167.489 87.9222 165.44 86.667 163.712 87.5479L154.788 92.0972C141.739 98.7494 133.523 112.159 133.523 126.806L133.523 194.719C133.533 199.06 128.821 201.787 125.061 199.638L129.432 202.163C133.336 204.384 137.666 205.489 142.003 205.487Z" fill="url(#paint9_linear_481_4810)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_481_4810" x1="207.413" y1="61.882" x2="148.999" y2="163.067" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9189F7"/>
|
||||
<stop offset="1" stop-color="#4135E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_481_4810" x1="188.235" y1="204.189" x2="264.21" y2="152.592" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4F42FD"/>
|
||||
<stop offset="1" stop-color="#7274FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_481_4810" x1="207.413" y1="61.882" x2="148.999" y2="163.067" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9189F7"/>
|
||||
<stop offset="1" stop-color="#4135E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_481_4810" x1="188.235" y1="204.189" x2="264.21" y2="152.592" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4F42FD"/>
|
||||
<stop offset="1" stop-color="#7274FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_481_4810" x1="93.1761" y1="128.826" x2="66.2399" y2="104.746" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.25" stop-color="#4F42FD"/>
|
||||
<stop offset="1" stop-color="#2C08AC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_481_4810" x1="50.9004" y1="195.826" x2="109.315" y2="94.6412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9189F7"/>
|
||||
<stop offset="1" stop-color="#4135E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_481_4810" x1="70.0787" y1="53.5188" x2="-5.89657" y2="105.116" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4F42FD"/>
|
||||
<stop offset="1" stop-color="#7274FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_481_4810" x1="50.9004" y1="195.826" x2="109.315" y2="94.6412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9189F7"/>
|
||||
<stop offset="1" stop-color="#4135E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_481_4810" x1="70.0787" y1="53.5188" x2="-5.89657" y2="105.116" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4F42FD"/>
|
||||
<stop offset="1" stop-color="#7274FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_481_4810" x1="165.135" y1="128.882" x2="192.072" y2="152.962" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.25" stop-color="#4F42FD"/>
|
||||
<stop offset="1" stop-color="#2C08AC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" transform="matrix(-1 0 0 1 256 0)" fill="white"/>
|
||||
<path d="M167.489 89.8618C167.489 87.9224 165.441 86.667 163.713 87.5473L154.788 92.0971C141.739 98.7493 133.523 112.159 133.523 126.806V194.718C133.533 199.053 128.837 201.777 125.08 199.647L84.2227 176.06C83.2036 175.472 82.5762 174.384 82.5762 173.207V106.445C82.5759 104.992 80.9999 104.087 79.7451 104.813L61.3467 115.428C53.4685 119.976 48.6096 128.382 48.6094 137.487V179.978C48.6094 189.074 53.4588 197.481 61.3467 202.039L67.3301 205.494C67.6627 205.707 68.0004 205.916 68.3447 206.115L95.3066 221.682C103.184 226.23 112.889 226.232 120.779 221.679L154.312 202.32C154.271 202.342 154.23 202.362 154.189 202.384C154.283 202.333 154.375 202.281 154.468 202.229L154.312 202.32C154.463 202.237 154.615 202.154 154.765 202.068L154.762 202.065C162.646 197.511 167.487 189.102 167.488 180.009L167.489 89.8618Z" fill="black"/>
|
||||
<path d="M163.007 36.0259C155.13 31.4781 145.424 31.4763 137.533 36.0288L104.002 55.3882C104.029 55.3732 104.057 55.359 104.084 55.3442C104.02 55.3794 103.956 55.4159 103.893 55.4516L104.002 55.3882C103.851 55.4714 103.699 55.5536 103.549 55.6401L103.552 55.6411C95.6664 60.1948 90.8241 68.6053 90.8232 77.6987L90.8223 167.846C90.8223 169.786 92.8707 171.04 94.5986 170.16L103.523 165.611C116.573 158.959 124.788 145.549 124.788 130.902V62.9897C124.778 58.6479 129.49 55.9211 133.25 58.0698L174.091 81.6479C175.11 82.2363 175.737 83.3238 175.737 84.5005V151.263C175.738 152.716 177.314 153.621 178.568 152.895L196.967 142.28C204.845 137.732 209.704 129.326 209.704 120.221V77.73C209.704 68.6343 204.855 60.2267 196.967 55.6694L190.982 52.2143C190.65 52.0011 190.313 51.792 189.969 51.5932L163.007 36.0259Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" transform="matrix(-1 0 0 1 256 0)" fill="black"/>
|
||||
<path d="M167.489 89.8618C167.489 87.9224 165.441 86.667 163.713 87.5473L154.788 92.0971C141.739 98.7493 133.523 112.159 133.523 126.806V194.718C133.533 199.053 128.837 201.777 125.08 199.647L84.2227 176.06C83.2036 175.472 82.5762 174.384 82.5762 173.207V106.445C82.5759 104.992 80.9999 104.087 79.7451 104.813L61.3467 115.428C53.4685 119.976 48.6096 128.382 48.6094 137.487V179.978C48.6094 189.074 53.4588 197.481 61.3467 202.039L67.3301 205.494C67.6627 205.707 68.0004 205.916 68.3447 206.115L95.3066 221.682C103.184 226.23 112.889 226.232 120.779 221.679L154.312 202.32C154.271 202.342 154.23 202.362 154.189 202.384C154.283 202.333 154.375 202.281 154.468 202.229L154.312 202.32C154.463 202.237 154.615 202.154 154.765 202.068L154.762 202.065C162.646 197.511 167.487 189.102 167.488 180.009L167.489 89.8618Z" fill="white"/>
|
||||
<path d="M163.007 36.0259C155.13 31.4781 145.424 31.4763 137.533 36.0288L104.002 55.3882C104.029 55.3732 104.057 55.359 104.084 55.3442C104.02 55.3794 103.956 55.4159 103.893 55.4516L104.002 55.3882C103.851 55.4714 103.699 55.5536 103.549 55.6401L103.552 55.6411C95.6664 60.1948 90.8241 68.6053 90.8232 77.6987L90.8223 167.846C90.8223 169.786 92.8707 171.04 94.5986 170.16L103.523 165.611C116.573 158.959 124.788 145.549 124.788 130.902V62.9897C124.778 58.6479 129.49 55.9211 133.25 58.0698L174.091 81.6479C175.11 82.2363 175.737 83.3238 175.737 84.5005V151.263C175.738 152.716 177.314 153.621 178.568 152.895L196.967 142.28C204.845 137.732 209.704 129.326 209.704 120.221V77.73C209.704 68.6343 204.855 60.2267 196.967 55.6694L190.982 52.2143C190.65 52.0011 190.313 51.792 189.969 51.5932L163.007 36.0259Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" transform="matrix(-1 0 0 1 256 0)" fill="white"/>
|
||||
<path d="M48.6094 179.978V137.487C48.6096 128.382 53.4685 119.976 61.3467 115.428L79.7451 104.813C80.9999 104.087 82.5759 104.992 82.5762 106.445V173.207C82.5762 174.384 83.2036 175.472 84.2227 176.06L125.08 199.647C128.837 201.777 133.533 199.053 133.523 194.718V126.806C133.523 112.388 141.484 99.1684 154.18 92.4135L154.788 92.0971L163.713 87.5473C165.441 86.667 167.489 87.9224 167.489 89.8618L167.488 180.009L167.474 180.86C167.181 189.624 162.399 197.653 154.762 202.065L154.765 202.068C154.615 202.154 154.463 202.237 154.312 202.32L154.468 202.229C154.375 202.281 154.283 202.333 154.189 202.384C154.23 202.362 154.271 202.342 154.312 202.32L120.779 221.679L120.034 222.092C112.534 226.093 103.539 226.092 96.0508 222.095L95.3066 221.682L68.3447 206.115C68.0004 205.916 67.6627 205.707 67.3301 205.494L61.3467 202.039C53.7053 197.624 48.9149 189.595 48.623 180.829L48.6094 179.978ZM175.737 84.5005C175.737 83.3974 175.186 82.3728 174.277 81.7641L174.091 81.6479L133.25 58.0698C129.49 55.9211 124.778 58.6479 124.788 62.9897V130.902L124.782 131.587C124.53 145.966 116.369 159.063 103.523 165.611L94.5986 170.16L94.4355 170.236C92.799 170.938 90.9459 169.803 90.8281 168.026L90.8223 167.846L90.8232 77.6987C90.8241 68.6053 95.6664 60.1948 103.552 55.6411L103.549 55.6401C103.699 55.5536 103.851 55.4714 104.002 55.3882L103.893 55.4516C103.956 55.4159 104.02 55.3794 104.084 55.3442C104.057 55.359 104.029 55.3732 104.002 55.3882L137.533 36.0288C145.424 31.4763 155.13 31.4781 163.007 36.0259L189.969 51.5932C190.313 51.792 190.65 52.0011 190.982 52.2143L196.967 55.6694C204.855 60.2267 209.704 68.6343 209.704 77.73V120.221L209.689 121.073C209.397 129.848 204.599 137.874 196.967 142.28L178.568 152.895C177.314 153.621 175.738 152.716 175.737 151.263V84.5005ZM176.814 149.866L176.819 149.864L176.832 149.856C176.826 149.859 176.82 149.862 176.814 149.866ZM137.023 194.71L137.019 195.038C136.802 201.878 129.341 206.087 123.354 202.692L123.33 202.678L82.4727 179.091C80.3698 177.877 79.0762 175.634 79.0762 173.207V109.239L63.0957 118.458C56.5124 122.259 52.3745 129.183 52.1221 136.752L52.1094 137.487V179.978C52.1094 187.823 56.2909 195.075 63.0957 199.007H63.0967L69.0801 202.462L69.1504 202.503L69.2188 202.547C69.5212 202.741 69.8111 202.92 70.0947 203.083L97.0566 218.651L97.6982 219.007C104.372 222.569 112.434 222.453 119.029 218.648L152.514 199.316L152.512 199.312C152.581 199.274 152.65 199.236 152.752 199.178L152.753 199.181C152.775 199.169 152.796 199.158 152.815 199.147L153.011 199.035C159.81 195.107 163.987 187.854 163.988 180.009V91.3344L156.378 95.2153C144.501 101.27 137.023 113.475 137.023 126.806V194.71ZM94.3223 166.372L101.934 162.493C113.811 156.438 121.288 144.233 121.288 130.902V62.9897C121.278 55.9521 128.901 51.5532 134.986 55.0307L135 55.0385L175.841 78.6167L176.036 78.7339C178.023 79.9714 179.237 82.15 179.237 84.5005V148.468L195.217 139.249C202.013 135.326 206.204 128.075 206.204 120.221V77.73C206.204 69.8848 202.022 62.6318 195.217 58.6997L189.232 55.2456L189.162 55.2046L189.094 55.1606C188.788 54.9649 188.5 54.787 188.219 54.6245L161.257 39.0571C154.463 35.135 146.091 35.1311 139.282 39.0591L139.283 39.06L105.765 58.4106L105.767 58.4135C105.713 58.443 105.723 58.4383 105.602 58.5063L105.6 58.5034C105.535 58.5391 105.481 58.5691 105.433 58.5962L105.302 58.6723C98.5016 62.5995 94.324 69.8532 94.3232 77.6987L94.3223 166.372Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" transform="matrix(-1 0 0 1 256 0)" fill="black"/>
|
||||
<path d="M48.6094 179.978V137.487C48.6096 128.382 53.4685 119.976 61.3467 115.428L79.7451 104.813C80.9999 104.087 82.5759 104.992 82.5762 106.445V173.207C82.5762 174.384 83.2036 175.472 84.2227 176.06L125.08 199.647C128.837 201.777 133.533 199.053 133.523 194.718V126.806C133.523 112.388 141.484 99.1684 154.18 92.4135L154.788 92.0971L163.713 87.5473C165.441 86.667 167.489 87.9224 167.489 89.8618L167.488 180.009L167.474 180.86C167.181 189.624 162.399 197.653 154.762 202.065L154.765 202.068C154.615 202.154 154.463 202.237 154.312 202.32L154.468 202.229C154.375 202.281 154.283 202.333 154.189 202.384C154.23 202.362 154.271 202.342 154.312 202.32L120.779 221.679L120.034 222.092C112.534 226.093 103.539 226.092 96.0508 222.095L95.3066 221.682L68.3447 206.115C68.0004 205.916 67.6627 205.707 67.3301 205.494L61.3467 202.039C53.7053 197.624 48.9149 189.595 48.623 180.829L48.6094 179.978ZM175.737 84.5005C175.737 83.3974 175.186 82.3728 174.277 81.7641L174.091 81.6479L133.25 58.0698C129.49 55.9211 124.778 58.6479 124.788 62.9897V130.902L124.782 131.587C124.53 145.966 116.369 159.063 103.523 165.611L94.5986 170.16L94.4355 170.236C92.799 170.938 90.9459 169.803 90.8281 168.026L90.8223 167.846L90.8232 77.6987C90.8241 68.6053 95.6664 60.1948 103.552 55.6411L103.549 55.6401C103.699 55.5536 103.851 55.4714 104.002 55.3882L103.893 55.4516C103.956 55.4159 104.02 55.3794 104.084 55.3442C104.057 55.359 104.029 55.3732 104.002 55.3882L137.533 36.0288C145.424 31.4763 155.13 31.4781 163.007 36.0259L189.969 51.5932C190.313 51.792 190.65 52.0011 190.982 52.2143L196.967 55.6694C204.855 60.2267 209.704 68.6343 209.704 77.73V120.221L209.689 121.073C209.397 129.848 204.599 137.874 196.967 142.28L178.568 152.895C177.314 153.621 175.738 152.716 175.737 151.263V84.5005ZM176.814 149.866L176.819 149.864L176.832 149.856C176.826 149.859 176.82 149.862 176.814 149.866ZM137.023 194.71L137.019 195.038C136.802 201.878 129.341 206.087 123.354 202.692L123.33 202.678L82.4727 179.091C80.3698 177.877 79.0762 175.634 79.0762 173.207V109.239L63.0957 118.458C56.5124 122.259 52.3745 129.183 52.1221 136.752L52.1094 137.487V179.978C52.1094 187.823 56.2909 195.075 63.0957 199.007H63.0967L69.0801 202.462L69.1504 202.503L69.2188 202.547C69.5212 202.741 69.8111 202.92 70.0947 203.083L97.0566 218.651L97.6982 219.007C104.372 222.569 112.434 222.453 119.029 218.648L152.514 199.316L152.512 199.312C152.581 199.274 152.65 199.236 152.752 199.178L152.753 199.181C152.775 199.169 152.796 199.158 152.815 199.147L153.011 199.035C159.81 195.107 163.987 187.854 163.988 180.009V91.3344L156.378 95.2153C144.501 101.27 137.023 113.475 137.023 126.806V194.71ZM94.3223 166.372L101.934 162.493C113.811 156.438 121.288 144.233 121.288 130.902V62.9897C121.278 55.9521 128.901 51.5532 134.986 55.0307L135 55.0385L175.841 78.6167L176.036 78.7339C178.023 79.9714 179.237 82.15 179.237 84.5005V148.468L195.217 139.249C202.013 135.326 206.204 128.075 206.204 120.221V77.73C206.204 69.8848 202.022 62.6318 195.217 58.6997L189.232 55.2456L189.162 55.2046L189.094 55.1606C188.788 54.9649 188.5 54.787 188.219 54.6245L161.257 39.0571C154.463 35.135 146.091 35.1311 139.282 39.0591L139.283 39.06L105.765 58.4106L105.767 58.4135C105.713 58.443 105.723 58.4383 105.602 58.5063L105.6 58.5034C105.535 58.5391 105.481 58.5691 105.433 58.5962L105.302 58.6723C98.5016 62.5995 94.324 69.8532 94.3232 77.6987L94.3223 166.372Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 136 KiB |
@@ -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/api-reference/agent/#strands.agent.agent_result.AgentResult) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/api-reference/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) |
|
||||
@@ -163,8 +163,8 @@ foreach (var update in response.Messages)
|
||||
### Option 2 Run: Container with Primary and Secondary Properties, RunStreaming: Stream of Primary + Secondary
|
||||
|
||||
Run returns a new response type that has separate properties for the Primary Content and the Secondary Updates leading up to it.
|
||||
The Primary content is available in the `AgentResponse.Messages` property while Secondary updates are in a new `AgentResponse.Updates` property.
|
||||
`AgentResponse.Text` returns the Primary content text.
|
||||
The Primary content is available in the `AgentRunResponse.Messages` property while Secondary updates are in a new `AgentRunResponse.Updates` property.
|
||||
`AgentRunResponse.Text` returns the Primary content text.
|
||||
|
||||
Since streaming would still need to return an `IAsyncEnumerable` of updates, the design would differ from non-streaming.
|
||||
With non-streaming Primary and Secondary content is split into separate lists, while with streaming it's combined in one stream.
|
||||
@@ -232,24 +232,24 @@ await foreach (var update in responses)
|
||||
```csharp
|
||||
class Agent
|
||||
{
|
||||
public abstract Task<AgentResponse> RunAsync(
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
class AgentResponse : ChatResponse
|
||||
class AgentRunResponse : ChatResponse
|
||||
{
|
||||
}
|
||||
|
||||
public class AgentResponseUpdate : ChatResponseUpdate
|
||||
public class AgentRunResponseUpdate : ChatResponseUpdate
|
||||
{
|
||||
}
|
||||
```
|
||||
@@ -265,20 +265,20 @@ The new types could also exclude properties that make less sense for agents, lik
|
||||
```csharp
|
||||
class Agent
|
||||
{
|
||||
public abstract Task<AgentResponse> RunAsync(
|
||||
public abstract Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
class AgentResponse // Compare with ChatResponse
|
||||
class AgentRunResponse // Compare with ChatResponse
|
||||
{
|
||||
public string Text { get; } // Aggregation of TextContent from messages.
|
||||
|
||||
@@ -294,12 +294,12 @@ class AgentResponse // Compare with ChatResponse
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
// Not Included in AgentResponse compared to ChatResponse
|
||||
// Not Included in AgentRunResponse compared to ChatResponse
|
||||
public ChatFinishReason? FinishReason { get; set; }
|
||||
public string? ConversationId { get; set; }
|
||||
public string? ModelId { get; set; }
|
||||
|
||||
public class AgentResponseUpdate // Compare with ChatResponseUpdate
|
||||
public class AgentRunResponseUpdate // Compare with ChatResponseUpdate
|
||||
{
|
||||
public string Text { get; } // Aggregation of TextContent from Contents.
|
||||
|
||||
@@ -317,7 +317,7 @@ public class AgentResponseUpdate // Compare with ChatResponseUpdate
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
// Not Included in AgentResponseUpdate compared to ChatResponseUpdate
|
||||
// Not Included in AgentRunResponseUpdate compared to ChatResponseUpdate
|
||||
public ChatFinishReason? FinishReason { get; set; }
|
||||
public string? ConversationId { get; set; }
|
||||
public string? ModelId { get; set; }
|
||||
@@ -360,7 +360,7 @@ public class ChatFinishReason
|
||||
### Option 2: Add another property on responses for AgentRun
|
||||
|
||||
```csharp
|
||||
class AgentResponse
|
||||
class AgentRunResponse
|
||||
{
|
||||
...
|
||||
public AgentRun RunReference { get; set; } // Reference to long running process
|
||||
@@ -368,7 +368,7 @@ class AgentResponse
|
||||
}
|
||||
|
||||
|
||||
public class AgentResponseUpdate
|
||||
public class AgentRunResponseUpdate
|
||||
{
|
||||
...
|
||||
public AgentRun RunReference { get; set; } // Reference to long running process
|
||||
@@ -424,7 +424,7 @@ Note that where an agent doesn't support structured output, it may also be possi
|
||||
See [Structured Outputs Support](#structured-outputs-support) for a comparison on what other agent frameworks and protocols support.
|
||||
|
||||
To support a good user experience for structured outputs, I'm proposing that we follow the pattern used by MEAI.
|
||||
We would add a generic version of `AgentResponse<T>`, that allows us to get the agent result already deserialized into our preferred type.
|
||||
We would add a generic version of `AgentRunResponse<T>`, that allows us to get the agent result already deserialized into our preferred type.
|
||||
This would be coupled with generic overload extension methods for Run that automatically builds a schema from the supplied type and updates
|
||||
the run options.
|
||||
|
||||
@@ -438,14 +438,14 @@ class Movie
|
||||
public int ReleaseYear { get; set; }
|
||||
}
|
||||
|
||||
AgentResponse<Movie[]> response = agent.RunAsync<Movie[]>("What are the top 3 children's movies of the 80s.");
|
||||
AgentRunResponse<Movie[]> response = agent.RunAsync<Movie[]>("What are the top 3 children's movies of the 80s.");
|
||||
Movie[] movies = response.Result
|
||||
```
|
||||
|
||||
If we only support requesting a schema at agent creation time or where an agent has a built in schema, the following would be the preferred approach:
|
||||
|
||||
```csharp
|
||||
AgentResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
|
||||
AgentRunResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
|
||||
Movie[] movies = response.TryParseStructuredOutput<Movie[]>();
|
||||
```
|
||||
|
||||
@@ -463,7 +463,7 @@ Option 2 chosen so that we can vary Agent responses independently of Chat Client
|
||||
### StructuredOutputs Decision
|
||||
|
||||
We will not support structured output per run request, but individual agents are free to allow this on the concrete implementation or at construction time.
|
||||
We will however add support for easily extracting a structured output type from the `AgentResponse`.
|
||||
We will however add support for easily extracting a structured output type from the `AgentRunResponse`.
|
||||
|
||||
## Addendum 1: AIContext Derived Types for different response types / Gap Analysis (Work in progress)
|
||||
|
||||
@@ -495,10 +495,10 @@ We need to decide what AIContent types, each agent response type will be mapped
|
||||
| SDK | Structured Outputs support |
|
||||
|-|-|
|
||||
| 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/) |
|
||||
| Google ADK | **Approach 1** Both [input and output shemas 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/latest/api-reference/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/api-reference/types/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) 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). |
|
||||
|
||||
@@ -54,7 +54,7 @@ The table below represents the majority of the naming changes discussed in issue
|
||||
| *Mcp* & *Http* | *MCP* & *HTTP* | accepted | Acronyms should be uppercased in class names, according to PEP 8. | None |
|
||||
| `agent.run_streaming` | `agent.run_stream` | accepted | Shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
|
||||
| `workflow.run_streaming` | `workflow.run_stream` | accepted | In sync with `agent.run_stream` and shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
|
||||
| AgentResponse & AgentResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
|
||||
| AgentRunResponse & AgentRunResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
|
||||
| *Content | * | rejected | Rejected other content type renames (removing `Content` suffix) because it would reduce clarity and discoverability. | Item was also considered, but rejected as it is very similar to Content, but would be inconsistent with dotnet. |
|
||||
| ChatResponse & ChatResponseUpdate | Response & ResponseUpdate | rejected | Rejected, because Response is too generic. | None |
|
||||
|
||||
|
||||
@@ -161,11 +161,11 @@ while (response.ApprovalRequests.Count > 0)
|
||||
response = await agent.RunAsync(messages, thread);
|
||||
}
|
||||
|
||||
class AgentResponse
|
||||
class AgentRunResponse
|
||||
{
|
||||
...
|
||||
|
||||
// A new property on AgentResponse to aggregate the ApprovalRequestContent items from
|
||||
// A new property on AgentRunResponse to aggregate the ApprovalRequestContent items from
|
||||
// the response messages (Similar to the Text property).
|
||||
public IEnumerable<ApprovalRequestContent> ApprovalRequests { get; set; }
|
||||
|
||||
@@ -251,11 +251,11 @@ while (response.UserInputRequests.Any())
|
||||
response = await agent.RunAsync(messages, thread);
|
||||
}
|
||||
|
||||
class AgentResponse
|
||||
class AgentRunResponse
|
||||
{
|
||||
...
|
||||
|
||||
// A new property on AgentResponse to aggregate the UserInputRequestContent items from
|
||||
// A new property on AgentRunResponse to aggregate the UserInputRequestContent items from
|
||||
// the response messages (Similar to the Text property).
|
||||
public IReadOnlyList<UserInputRequestContent> UserInputRequests { get; set; }
|
||||
|
||||
@@ -366,11 +366,11 @@ while (response.UserInputRequests.Any())
|
||||
response = await agent.RunAsync(messages, thread);
|
||||
}
|
||||
|
||||
class AgentResponse
|
||||
class AgentRunResponse
|
||||
{
|
||||
...
|
||||
|
||||
// A new property on AgentResponse to aggregate the UserInputRequestContent items from
|
||||
// A new property on AgentRunResponse to aggregate the UserInputRequestContent items from
|
||||
// the response messages (Similar to the Text property).
|
||||
public IEnumerable<UserInputRequestContent> UserInputRequests { get; set; }
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ public class AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AgentResponse> RunAsync(
|
||||
public async Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -135,7 +135,7 @@ public class AIAgent
|
||||
return context.Response ?? throw new InvalidOperationException("Agent execution did not produce a response");
|
||||
}
|
||||
|
||||
protected abstract Task<AgentResponse> ExecuteCoreLogicAsync(
|
||||
protected abstract Task<AgentRunResponse> ExecuteCoreLogicAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread,
|
||||
AgentRunOptions? options,
|
||||
@@ -190,7 +190,7 @@ internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
|
||||
|
||||
public GuardrailCallbackAgent(AIAgent innerAgent) : base(innerAgent) { }
|
||||
|
||||
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filteredMessages = this.FilterMessages(messages);
|
||||
Console.WriteLine($"Guardrail Middleware - Filtered messages: {new ChatResponse(filteredMessages).Text}");
|
||||
@@ -202,14 +202,14 @@ internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filteredMessages = this.FilterMessages(messages);
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(filteredMessages, thread, options, cancellationToken))
|
||||
{
|
||||
if (update.Text != null)
|
||||
{
|
||||
yield return new AgentResponseUpdate(update.Role, this.FilterContent(update.Text));
|
||||
yield return new AgentRunResponseUpdate(update.Role, this.FilterContent(update.Text));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -252,7 +252,7 @@ internal sealed class RunningCallbackHandlerAgent : DelegatingAIAgent
|
||||
this._func = func;
|
||||
}
|
||||
|
||||
public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = new AgentInvokeCallbackContext(this, messages, thread, options, isStreaming: false, cancellationToken);
|
||||
|
||||
@@ -469,7 +469,7 @@ public sealed class CallbackEnabledAgent : DelegatingAIAgent
|
||||
this._callbacksProcessor = callbackMiddlewareProcessor ?? new();
|
||||
}
|
||||
|
||||
public override async Task<AgentResponse> RunAsync(
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
@@ -541,7 +541,7 @@ public abstract class AgentContext
|
||||
public class AgentRunContext : AgentContext
|
||||
{
|
||||
public IList<ChatMessage> Messages { get; set; }
|
||||
public AgentResponse? Response { get; set; }
|
||||
public AgentRunResponse? Response { get; set; }
|
||||
public AgentThread? Thread { get; }
|
||||
|
||||
public AgentRunContext(AIAgent agent, IList<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options)
|
||||
@@ -1125,7 +1125,7 @@ Naming (Python): N/A (Composable Components)
|
||||
Supports: N
|
||||
Observation: No explicit middleware/filters; modularity allows composable units but no dedicated interception hooks or callbacks for custom reading/modification mid-execution.
|
||||
|
||||
No specific code examples available for interception.
|
||||
For more details, see the official documentation: [Atomic Agents Docs](https://brainblend-ai.github.io/atomic-agents/). No specific code examples available for interception.
|
||||
|
||||
#### Smolagents (Hugging Face)
|
||||
|
||||
|
||||
@@ -687,7 +687,7 @@ This section considers different options for exposing the `RunId`, `Status`, and
|
||||
#### 4.1. As AIContent
|
||||
|
||||
The `AsyncRunContent` class will represent a long-running operation initiated and managed by an agent/LLM.
|
||||
Items of this content type will be returned in a chat message as part of the `AgentResponse` or `ChatResponse`
|
||||
Items of this content type will be returned in a chat message as part of the `AgentRunResponse` or `ChatResponse`
|
||||
response to represent the long-running operation.
|
||||
|
||||
The `AsyncRunContent` class has two properties: `RunId` and `Status`. The `RunId` identifies the
|
||||
@@ -1162,29 +1162,29 @@ For cancellation and deletion of long-running operations, new methods will be ad
|
||||
public abstract class AIAgent
|
||||
{
|
||||
// Existing methods...
|
||||
public Task<AgentResponse> RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
public Task<AgentRunResponse> RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
|
||||
|
||||
// New methods for uncommon operations
|
||||
public virtual Task<AgentResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public virtual Task<AgentRunResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<AgentResponse?>(null);
|
||||
return Task.FromResult<AgentRunResponse?>(null);
|
||||
}
|
||||
|
||||
public virtual Task<AgentResponse?> DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public virtual Task<AgentRunResponse?> DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<AgentResponse?>(null);
|
||||
return Task.FromResult<AgentRunResponse?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Agent that supports update and cancellation
|
||||
public class CustomAgent : AIAgent
|
||||
{
|
||||
public override async Task<AgentResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<AgentRunResponse?> CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await this._client.CancelRunAsync(id, options?.Thread?.ConversationId);
|
||||
|
||||
return ConvertToAgentResponse(response);
|
||||
return ConvertToAgentRunResponse(response);
|
||||
}
|
||||
|
||||
// No overload for DeleteRunAsync as it's not supported by the underlying API
|
||||
@@ -1195,7 +1195,7 @@ AIAgent agent = new CustomAgent();
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
AgentResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
AgentRunResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
|
||||
response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOptions { Thread = thread });
|
||||
```
|
||||
@@ -1251,10 +1251,10 @@ public class AgentRunOptions
|
||||
AIAgent agent = ...; // Get an instance of an AIAgent
|
||||
|
||||
// Start a long-running execution for the prompt if supported by the underlying API
|
||||
AgentResponse response = await agent.RunAsync("<prompt>", new AgentRunOptions { AllowLongRunningResponses = true });
|
||||
AgentRunResponse response = await agent.RunAsync("<prompt>", new AgentRunOptions { AllowLongRunningResponses = true });
|
||||
|
||||
// Start a quick prompt
|
||||
AgentResponse response = await agent.RunAsync("<prompt>");
|
||||
AgentRunResponse response = await agent.RunAsync("<prompt>");
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
@@ -1279,7 +1279,7 @@ Below are the details of the option selected for chat clients that is also selec
|
||||
#### 3.1 Continuation Token of a Custom Type
|
||||
|
||||
This option suggests using `ContinuationToken` to encapsulate all properties representing a long-running operation. The continuation token will be returned by agents in the
|
||||
`ContinuationToken` property of the `AgentResponse` and `AgentResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
|
||||
`ContinuationToken` property of the `AgentRunResponse` and `AgentRunResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
|
||||
of the property will indicate that the response is not part of a long-running operation or the long-running operation has been completed. Callers will set the token in the
|
||||
`ContinuationToken` property of the `AgentRunOptions` class in follow-up calls to the `Run{Streaming}Async` methods to indicate that they want to "continue" the long-running
|
||||
operation identified by the token.
|
||||
@@ -1313,18 +1313,18 @@ public class AgentRunOptions
|
||||
public ResponseContinuationToken? ContinuationToken { get; set; }
|
||||
}
|
||||
|
||||
public class AgentResponse
|
||||
public class AgentRunResponse
|
||||
{
|
||||
public ResponseContinuationToken? ContinuationToken { get; }
|
||||
}
|
||||
|
||||
public class AgentResponseUpdate
|
||||
public class AgentRunResponseUpdate
|
||||
{
|
||||
public ResponseContinuationToken? ContinuationToken { get; }
|
||||
}
|
||||
|
||||
// Usage example
|
||||
AgentResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
AgentRunResponse response = await agent.RunAsync("What is the capital of France?");
|
||||
|
||||
AgentRunOptions options = new() { ContinuationToken = response.ContinuationToken };
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Chosen option: "Current approach with internal event types and framework-native
|
||||
|
||||
- Protects consumers from protocol changes by keeping AG-UI events internal
|
||||
- Maintains framework abstractions through conversion at boundaries
|
||||
- Uses existing framework types (AgentResponseUpdate, ChatMessage) for public API
|
||||
- Uses existing framework types (AgentRunResponseUpdate, ChatMessage) for public API
|
||||
- Focuses on core text streaming functionality
|
||||
- Leverages existing properties (ConversationId, ResponseId, ErrorContent) instead of custom types
|
||||
- Provides bidirectional client and server support
|
||||
@@ -69,7 +69,7 @@ Chosen option: "Current approach with internal event types and framework-native
|
||||
|
||||
3. **Agent Factory Pattern** - `MapAGUIAgent` uses factory function `(messages) => AIAgent` to allow request-specific agent configuration supporting multi-tenancy
|
||||
|
||||
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentResponseUpdate`)
|
||||
4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentRunResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentRunResponseUpdate`)
|
||||
|
||||
5. **Thread Management** - `AGUIAgentThread` stores only `ThreadId` with thread ID communicated via `ConversationId`; applications manage persistence for parity with other implementations and to be compliant with the protocol. Future extensions will support having the server manage the conversation.
|
||||
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: dmytrostruk
|
||||
date: 2025-12-12
|
||||
deciders: dmytrostruk, markwallace-microsoft, eavanvalkenburg, giles17
|
||||
---
|
||||
|
||||
# Create/Get Agent API
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
There is a misalignment between the create/get agent API in the .NET and Python implementations.
|
||||
|
||||
In .NET, the `CreateAIAgent` method can create either a local instance of an agent or a remote instance if the backend provider supports it. For remote agents, once the agent is created, you can retrieve an existing remote agent by using the `GetAIAgent` method. If a backend provider doesn't support remote agents, `CreateAIAgent` just initializes a new local agent instance and `GetAIAgent` is not available. There is also a `BuildAIAgent` method, which is an extension for the `ChatClientBuilder` class from `Microsoft.Extensions.AI`. It builds pipelines of `IChatClient` instances with an `IServiceProvider`. This functionality does not exist in Python, so `BuildAIAgent` is out of scope.
|
||||
|
||||
In Python, there is only one `create_agent` method, which always creates a local instance of the agent. If the backend provider supports remote agents, the remote agent is created only on the first `agent.run()` invocation.
|
||||
|
||||
Below is a short summary of different providers and their APIs in .NET:
|
||||
|
||||
| Package | Method | Behavior | Python support |
|
||||
|---|---|---|---|
|
||||
| Microsoft.Agents.AI | `CreateAIAgent` (based on `IChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.Anthropic | `CreateAIAgent` (based on `IBetaService` and `IAnthropicClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`AnthropicClient` inherits `BaseChatClient`, which exposes `create_agent`). |
|
||||
| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent` (based on `AIProjectClient` with `AgentReference`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent`/`GetAIAgentAsync` (with `Name`/`ChatClientAgentOptions`) | Fetches `AgentRecord` via HTTP, then creates a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.AzureAI (V2) | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AIProjectClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent` (based on `PersistentAgentsClient` with `PersistentAgent`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `PersistentAgent` via HTTP, then creates a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `CreateAIAgent`/`CreateAIAgentAsync` | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.OpenAI | `GetAIAgent` (based on `AssistantClient` with `Assistant`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.OpenAI | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `Assistant` via HTTP, then creates a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AssistantClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
|
||||
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `ChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
|
||||
| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `OpenAIResponseClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
|
||||
|
||||
Another difference between Python and .NET implementation is that in .NET `CreateAIAgent`/`GetAIAgent` methods are implemented as extension methods based on underlying SDK client, like `AIProjectClient` from Azure AI or `AssistantClient` from OpenAI:
|
||||
|
||||
```csharp
|
||||
// Definition
|
||||
public static ChatClientAgent CreateAIAgent(
|
||||
this AIProjectClient aiProjectClient,
|
||||
string name,
|
||||
string model,
|
||||
string instructions,
|
||||
string? description = null,
|
||||
IList<AITool>? tools = null,
|
||||
Func<IChatClient, IChatClient>? clientFactory = null,
|
||||
IServiceProvider? services = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{ }
|
||||
|
||||
// Usage
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); // Initialization of underlying SDK client
|
||||
|
||||
var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AgentName, model: deploymentName, instructions: AgentInstructions, tools: [tool]); // ChatClientAgent creation from underlying SDK client
|
||||
|
||||
// Alternative usage (same as extension method, just explicit syntax)
|
||||
var newAgent = await AzureAIProjectChatClientExtensions.CreateAIAgentAsync(
|
||||
aiProjectClient,
|
||||
name: AgentName,
|
||||
model: deploymentName,
|
||||
instructions: AgentInstructions,
|
||||
tools: [tool]);
|
||||
```
|
||||
|
||||
Python doesn't support extension methods. Currently `create_agent` method is defined on `BaseChatClient`, but this method only creates a local instance of `ChatAgent` and it can't create remote agents for providers that support it for a couple of reasons:
|
||||
|
||||
- It's defined as non-async.
|
||||
- `BaseChatClient` implementation is stateful for providers like Azure AI or OpenAI Assistants. The implementation stores agent/assistant metadata like `AgentId` and `AgentName`, so currently it's not possible to create different instances of `ChatAgent` from a single `BaseChatClient` in case if the implementation is stateful.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- API should be aligned between .NET and Python.
|
||||
- API should be intuitive and consistent between backend providers in .NET and Python.
|
||||
|
||||
## Considered Options
|
||||
|
||||
Add missing implementations on the Python side. This should include the following:
|
||||
|
||||
### agent-framework-azure-ai (both V1 and V2)
|
||||
|
||||
- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
|
||||
- Add a `get_agent` method that accepts an agent identifier, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
|
||||
- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
|
||||
|
||||
.NET:
|
||||
|
||||
```csharp
|
||||
var agent1 = new AIProjectClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from Azure.AI.Projects.OpenAI.AgentReference
|
||||
var agent2 = new AIProjectClient(...).GetAIAgent(agentName); // Fetches agent data, creates a local ChatClientAgent instance
|
||||
var agent3 = new AIProjectClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
|
||||
```
|
||||
|
||||
### agent-framework-core (OpenAI Assistants)
|
||||
|
||||
- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
|
||||
- Add a `get_agent` method that accepts an agent name, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
|
||||
- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
|
||||
|
||||
.NET:
|
||||
|
||||
```csharp
|
||||
var agent1 = new AssistantClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from OpenAI.Assistants.Assistant
|
||||
var agent2 = new AssistantClient(...).GetAIAgent(agentId); // Fetches agent data, creates a local ChatClientAgent instance
|
||||
var agent3 = new AssistantClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
|
||||
```
|
||||
|
||||
### Possible Python implementations
|
||||
|
||||
Methods like `create_agent` and `get_agent` should be implemented separately or defined on some stateless component that will allow to create multiple agents from the same instance/place.
|
||||
|
||||
Possible options:
|
||||
|
||||
#### Option 1: Module-level functions
|
||||
|
||||
Implement free functions in the provider package that accept the underlying SDK client as the first argument (similar to .NET extension methods, but expressed in Python).
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from agent_framework.azure import create_agent, get_agent
|
||||
|
||||
ai_project_client = AIProjectClient(...)
|
||||
|
||||
# Creates a remote agent first, then returns a local ChatAgent wrapper
|
||||
created_agent = await create_agent(
|
||||
ai_project_client,
|
||||
name="",
|
||||
instructions="",
|
||||
tools=[tool],
|
||||
)
|
||||
|
||||
# Gets an existing remote agent and returns a local ChatAgent wrapper
|
||||
first_agent = await get_agent(ai_project_client, agent_id=agent_id)
|
||||
|
||||
# Wraps an SDK agent instance (no extra HTTP call)
|
||||
second_agent = get_agent(ai_project_client, agent_reference)
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- Naturally supports async `create_agent` / `get_agent`.
|
||||
- Supports multiple agents per SDK client.
|
||||
- Closest conceptual match to .NET extension methods while staying Pythonic.
|
||||
|
||||
Cons:
|
||||
|
||||
- Discoverability is lower (users need to know where the functions live).
|
||||
- Verbose when creating multiple agents (client must be passed every time):
|
||||
|
||||
```python
|
||||
agent1 = await azure_agents.create_agent(client, name="Agent1", ...)
|
||||
agent2 = await azure_agents.create_agent(client, name="Agent2", ...)
|
||||
```
|
||||
|
||||
#### Option 2: Provider object
|
||||
|
||||
Introduce a dedicated provider type that is constructed from the underlying SDK client, and exposes async `create_agent` / `get_agent` methods.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIAgentProvider
|
||||
|
||||
ai_project_client = AIProjectClient(...)
|
||||
provider = AzureAIAgentProvider(ai_project_client)
|
||||
|
||||
agent = await provider.create_agent(
|
||||
name="",
|
||||
instructions="",
|
||||
tools=[tool],
|
||||
)
|
||||
|
||||
agent = await provider.get_agent(agent_id=agent_id)
|
||||
agent = provider.get_agent(agent_reference=agent_reference)
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- High discoverability and clear grouping of related behavior.
|
||||
- Keeps SDK clients unchanged and supports multiple agents per SDK client.
|
||||
- Concise when creating multiple agents (client passed once):
|
||||
|
||||
```python
|
||||
provider = AzureAIAgentProvider(ai_project_client)
|
||||
agent1 = await provider.create_agent(name="Agent1", ...)
|
||||
agent2 = await provider.create_agent(name="Agent2", ...)
|
||||
```
|
||||
|
||||
Cons:
|
||||
|
||||
- Adds a new public concept/type for users to learn.
|
||||
|
||||
#### Option 3: Inheritance (SDK client subclass)
|
||||
|
||||
Create a subclass of the underlying SDK client and add `create_agent` / `get_agent` methods.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
class ExtendedAIProjectClient(AIProjectClient):
|
||||
async def create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
|
||||
...
|
||||
|
||||
async def get_agent(self, *, agent_id: str | None = None, sdk_agent=None, **kwargs) -> ChatAgent:
|
||||
...
|
||||
|
||||
client = ExtendedAIProjectClient(...)
|
||||
agent = await client.create_agent(name="", instructions="")
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- Discoverable and ergonomic call sites.
|
||||
- Mirrors the .NET “methods on the client” feeling.
|
||||
|
||||
Cons:
|
||||
|
||||
- Many SDK clients are not designed for inheritance; SDK upgrades can break subclasses.
|
||||
- Users must opt into subclass everywhere.
|
||||
- Typing/initialization can be tricky if the SDK client has non-trivial constructors.
|
||||
|
||||
#### Option 4: Monkey patching
|
||||
|
||||
Attach `create_agent` / `get_agent` methods to an SDK client class (or instance) at runtime.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
def _create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
|
||||
...
|
||||
|
||||
AIProjectClient.create_agent = _create_agent # monkey patch
|
||||
```
|
||||
|
||||
Pros:
|
||||
|
||||
- Produces “extension method-like” call sites without wrappers or subclasses.
|
||||
|
||||
Cons:
|
||||
|
||||
- Fragile across SDK updates and difficult to type-check.
|
||||
- Surprising behavior (global side effects), potential conflicts across packages.
|
||||
- Harder to support/debug, especially in larger apps and test suites.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Implement `create_agent`/`get_agent`/`as_agent` API via **Option 2: Provider object**.
|
||||
|
||||
### Rationale
|
||||
|
||||
| Aspect | Option 1 (Functions) | Option 2 (Provider) |
|
||||
|--------|----------------------|---------------------|
|
||||
| Multiple implementations | One package may contain V1, V2, and other agent types. Function names like `create_agent` become ambiguous - which agent type does it create? | Each provider class is explicit: `AzureAIAgentsProvider` vs `AzureAIProjectAgentProvider` |
|
||||
| Discoverability | Users must know to import specific functions from the package | IDE autocomplete on provider instance shows all available methods |
|
||||
| Client reuse | SDK client must be passed to every function call: `create_agent(client, ...)`, `get_agent(client, ...)` | SDK client passed once at construction: `provider = Provider(client)` |
|
||||
|
||||
**Option 1 example:**
|
||||
```python
|
||||
from agent_framework.azure import create_agent, get_agent
|
||||
agent1 = await create_agent(client, name="Agent1", ...) # Which agent type, V1 or V2?
|
||||
agent2 = await create_agent(client, name="Agent2", ...) # Repetitive client passing
|
||||
```
|
||||
|
||||
**Option 2 example:**
|
||||
```python
|
||||
from agent_framework.azure import AzureAIProjectAgentProvider
|
||||
provider = AzureAIProjectAgentProvider(client) # Clear which service, client passed once
|
||||
agent1 = await provider.create_agent(name="Agent1", ...)
|
||||
agent2 = await provider.create_agent(name="Agent2", ...)
|
||||
```
|
||||
|
||||
### Method Naming
|
||||
|
||||
| Operation | Python | .NET | Async |
|
||||
|-----------|--------|------|-------|
|
||||
| Create on service | `create_agent()` | `CreateAIAgent()` | Yes |
|
||||
| Get from service | `get_agent(id=...)` | `GetAIAgent(agentId)` | Yes |
|
||||
| Wrap SDK object | `as_agent(reference)` | `AsAIAgent(agentInstance)` | No |
|
||||
|
||||
The method names (`create_agent`, `get_agent`) do not explicitly mention "service" or "remote" because:
|
||||
- In Python, the provider class name explicitly identifies the service (`AzureAIAgentsProvider`, `OpenAIAssistantProvider`), making additional qualifiers in method names redundant.
|
||||
- In .NET, these are extension methods on `AIProjectClient` or `AssistantClient`, which already imply service operations.
|
||||
|
||||
### Provider Class Naming
|
||||
|
||||
| Package | Provider Class | SDK Client | Service |
|
||||
|---------|---------------|------------|---------|
|
||||
| `agent_framework.azure` | `AzureAIProjectAgentProvider` | `AIProjectClient` | Azure AI Agent Service, based on Responses API (V2) |
|
||||
| `agent_framework.azure` | `AzureAIAgentsProvider` | `AgentsClient` | Azure AI Agent Service (V1) |
|
||||
| `agent_framework.openai` | `OpenAIAssistantProvider` | `AsyncOpenAI` | OpenAI Assistants API |
|
||||
|
||||
> **Note:** Azure AI naming is temporary. Final naming will be updated according to Azure AI / Microsoft Foundry renaming decisions.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Azure AI Agent Service V2 (based on Responses API)
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIProjectAgentProvider
|
||||
from azure.ai.projects import AIProjectClient
|
||||
|
||||
client = AIProjectClient(endpoint, credential)
|
||||
provider = AzureAIProjectAgentProvider(client)
|
||||
|
||||
# Create new agent on service
|
||||
agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
|
||||
|
||||
# Get existing agent by name
|
||||
agent = await provider.get_agent(agent_name="MyAgent")
|
||||
|
||||
# Wrap already-fetched SDK object (no HTTP calls)
|
||||
agent_ref = await client.agents.get("MyAgent")
|
||||
agent = provider.as_agent(agent_ref)
|
||||
```
|
||||
|
||||
#### Azure AI Persistent Agents V1
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIAgentsProvider
|
||||
from azure.ai.agents import AgentsClient
|
||||
|
||||
client = AgentsClient(endpoint, credential)
|
||||
provider = AzureAIAgentsProvider(client)
|
||||
|
||||
agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
|
||||
agent = await provider.get_agent(agent_id="persistent-agent-456")
|
||||
agent = provider.as_agent(persistent_agent)
|
||||
```
|
||||
|
||||
#### OpenAI Assistants
|
||||
|
||||
```python
|
||||
from agent_framework.openai import OpenAIAssistantProvider
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
provider = OpenAIAssistantProvider(client)
|
||||
|
||||
agent = await provider.create_agent(name="MyAssistant", model="gpt-4", instructions="...")
|
||||
agent = await provider.get_agent(assistant_id="asst_123")
|
||||
agent = provider.as_agent(assistant)
|
||||
```
|
||||
|
||||
#### Local-Only Agents (No Provider)
|
||||
|
||||
Current method `create_agent` (python) / `CreateAIAgent` (.NET) can be renamed to `as_agent` (python) / `AsAIAgent` (.NET) to emphasize the conversion logic rather than creation/initialization logic and to avoid collision with `create_agent` method for remote calls.
|
||||
|
||||
```python
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
# Convert chat client to ChatAgent (no remote service involved)
|
||||
client = OpenAIChatClient(model="gpt-4")
|
||||
agent = client.as_agent(name="LocalAgent", instructions="...") # instead of create_agent
|
||||
```
|
||||
|
||||
### Adding New Agent Types
|
||||
|
||||
Python:
|
||||
|
||||
1. Create provider class in appropriate package.
|
||||
2. Implement `create_agent`, `get_agent`, `as_agent` as applicable.
|
||||
|
||||
.NET:
|
||||
|
||||
1. Create static class for extension methods.
|
||||
2. Implement `CreateAIAgentAsync`, `GetAIAgentAsync`, `AsAIAgent` as applicable.
|
||||
@@ -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,258 +0,0 @@
|
||||
---
|
||||
status: Accepted
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-01-06
|
||||
deciders: markwallace-microsoft, dmytrostruk, taochenosu, alliscode, moonbox3, sphenry
|
||||
consulted: sergeymenshykh, rbarreto, dmytrostruk, westey-m
|
||||
informed:
|
||||
---
|
||||
|
||||
# Simplify Python Get Response API into a single method
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Currently chat clients must implement two separate methods to get responses, one for streaming and one for non-streaming. This adds complexity to the client implementations and increases the maintenance burden. This was likely done because the .NET version cannot do proper typing with a single method, in Python this is possible and this for instance is also how the OpenAI python client works, this would then also make it simpler to work with the Python version because there is only one method to learn about instead of two.
|
||||
|
||||
## Implications of this change
|
||||
|
||||
### Current Architecture Overview
|
||||
|
||||
The current design has **two separate methods** at each layer:
|
||||
|
||||
| Layer | Non-streaming | Streaming |
|
||||
|-------|---------------|-----------|
|
||||
| **Protocol** | `get_response()` → `ChatResponse` | `get_streaming_response()` → `AsyncIterable[ChatResponseUpdate]` |
|
||||
| **BaseChatClient** | `get_response()` (public) | `get_streaming_response()` (public) |
|
||||
| **Implementation** | `_inner_get_response()` (private) | `_inner_get_streaming_response()` (private) |
|
||||
|
||||
### Key Usage Areas Identified
|
||||
|
||||
#### 1. **ChatAgent** (_agents.py)
|
||||
- `run()` → calls `self.chat_client.get_response()`
|
||||
- `run_stream()` → calls `self.chat_client.get_streaming_response()`
|
||||
|
||||
These are parallel methods on the agent, so consolidating the client methods would **not break** the agent API. You could keep `agent.run()` and `agent.run_stream()` unchanged while internally calling `get_response(stream=True/False)`.
|
||||
|
||||
#### 2. **Function Invocation Decorator** (_tools.py)
|
||||
This is **the most impacted area**. Currently:
|
||||
- `_handle_function_calls_response()` decorates `get_response`
|
||||
- `_handle_function_calls_streaming_response()` decorates `get_streaming_response`
|
||||
- The `use_function_invocation` class decorator wraps **both methods separately**
|
||||
|
||||
**Impact**: The decorator logic is almost identical (~200 lines each) with small differences:
|
||||
- Non-streaming collects response, returns it
|
||||
- Streaming yields updates, returns async iterable
|
||||
|
||||
With a unified method, you'd need **one decorator** that:
|
||||
- Checks the `stream` parameter
|
||||
- Uses `@overload` to determine return type
|
||||
- Handles both paths with conditional logic
|
||||
- The new decorator could be applied just on the method, instead of the whole class.
|
||||
|
||||
This would **reduce code duplication** but add complexity to a single function.
|
||||
|
||||
#### 3. **Observability/Instrumentation** (observability.py)
|
||||
Same pattern as function invocation:
|
||||
- `_trace_get_response()` wraps `get_response`
|
||||
- `_trace_get_streaming_response()` wraps `get_streaming_response`
|
||||
- `use_instrumentation` decorator applies both
|
||||
|
||||
**Impact**: Would need consolidation into a single tracing wrapper.
|
||||
|
||||
#### 4. **Chat Middleware** (_middleware.py)
|
||||
The `use_chat_middleware` decorator also wraps both methods separately with similar logic.
|
||||
|
||||
#### 5. **AG-UI Client** (_client.py)
|
||||
Wraps both methods to unwrap server function calls:
|
||||
```python
|
||||
original_get_streaming_response = chat_client.get_streaming_response
|
||||
original_get_response = chat_client.get_response
|
||||
```
|
||||
|
||||
#### 6. **Provider Implementations** (all subpackages)
|
||||
All subclasses implement both `_inner_*` methods, except:
|
||||
- OpenAI Assistants Client (and similar clients, such as Foundry Agents V1) - it implements `_inner_get_response` by calling `_inner_get_streaming_response`
|
||||
|
||||
### Implications of Consolidation
|
||||
|
||||
| Aspect | Impact |
|
||||
|--------|--------|
|
||||
| **Type Safety** | Overloads work well: `@overload` with `Literal[True]` → `AsyncIterable`, `Literal[False]` → `ChatResponse`. Runtime return type based on `stream` param. |
|
||||
| **Breaking Change** | **Major breaking change** for anyone implementing custom chat clients. They'd need to update from 2 methods to 1 (or 2 inner methods to 1). |
|
||||
| **Decorator Complexity** | All 3 decorator systems (function invocation, middleware, observability) would need refactoring to handle both paths in one wrapper. |
|
||||
| **Code Reduction** | Significant reduction in _tools.py (~200 lines of near-duplicate code) and other decorators. |
|
||||
| **Samples/Tests** | Many samples call `get_streaming_response()` directly - would need updates. |
|
||||
| **Protocol Simplification** | `ChatClientProtocol` goes from 2 methods + 1 property to 1 method + 1 property. |
|
||||
|
||||
### Recommendation
|
||||
|
||||
The consolidation makes sense architecturally, but consider:
|
||||
|
||||
1. **The overload pattern with `stream: bool`** works well in Python typing:
|
||||
```python
|
||||
@overload
|
||||
async def get_response(self, messages, *, stream: Literal[True] = True, ...) -> AsyncIterable[ChatResponseUpdate]: ...
|
||||
@overload
|
||||
async def get_response(self, messages, *, stream: Literal[False] = False, ...) -> ChatResponse: ...
|
||||
```
|
||||
|
||||
2. **The decorator complexity** is the biggest concern. The current approach of separate decorators for separate methods is cleaner than conditional logic inside one wrapper.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Reduce code needed to implement a Chat Client, simplify the public API for chat clients
|
||||
- Reduce code duplication in decorators and middleware
|
||||
- Maintain type safety and clarity in method signatures
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. Status quo: Keep separate methods for streaming and non-streaming
|
||||
2. Consolidate into a single `get_response` method with a `stream` parameter
|
||||
3. Option 2 plus merging `agent.run` and `agent.run_stream` into a single method with a `stream` parameter as well
|
||||
|
||||
## Option 1: Status Quo
|
||||
- Good: Clear separation of streaming vs non-streaming logic
|
||||
- Good: Aligned with .NET design, although it is already `run` for Python and `RunAsync` for .NET
|
||||
- Bad: Code duplication in decorators and middleware
|
||||
- Bad: More complex client implementations
|
||||
|
||||
## Option 2: Consolidate into Single Method
|
||||
- Good: Simplified public API for chat clients
|
||||
- Good: Reduced code duplication in decorators
|
||||
- Good: Smaller API footprint for users to get familiar with
|
||||
- Good: People using OpenAI directly already expect this pattern
|
||||
- Bad: Increased complexity in decorators and middleware
|
||||
- Bad: Less alignment with .NET design (`get_response(stream=True)` vs `GetStreamingResponseAsync`)
|
||||
|
||||
## Option 3: Consolidate + Merge Agent and Workflow Methods
|
||||
- Good: Further simplifies agent and workflow implementation
|
||||
- Good: Single method for all chat interactions
|
||||
- Good: Smaller API footprint for users to get familiar with
|
||||
- Good: People using OpenAI directly already expect this pattern
|
||||
- Good: Workflows internally already use a single method (_run_workflow_with_tracing), so would eliminate public API duplication as well, with hardly any code changes
|
||||
- Bad: More breaking changes for agent users
|
||||
- Bad: Increased complexity in agent implementation
|
||||
- Bad: More extensive misalignment with .NET design (`run(stream=True)` vs `RunStreamingAsync` in addition to `get_response` change)
|
||||
|
||||
## Misc
|
||||
|
||||
Smaller questions to consider:
|
||||
- Should default be `stream=False` or `stream=True`? (Current is False)
|
||||
- Default to `False` makes it simpler for new users, as non-streaming is easier to handle.
|
||||
- Default to `False` aligns with existing behavior.
|
||||
- Streaming tends to be faster, so defaulting to `True` could improve performance for common use cases.
|
||||
- Should this differ between ChatClient, Agent and Workflows? (e.g., Agent and Workflow defaults to streaming, ChatClient to non-streaming)
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen Option: **Option 3: Consolidate + Merge Agent and Workflow Methods**
|
||||
|
||||
Since this is the most pythonic option and it reduces the API surface and code duplication the most, we will go with this option.
|
||||
We will keep the default of `stream=False` for all methods to maintain backward compatibility and simplicity for new users.
|
||||
|
||||
# Appendix
|
||||
## Code Samples for Consolidated Method
|
||||
|
||||
### Python - Option 3: Direct ChatClient + Agent with Single Method
|
||||
|
||||
```python
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Example 1: Direct ChatClient usage with single method
|
||||
client = OpenAIChatClient()
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
|
||||
# Non-streaming usage
|
||||
print(f"User: {message}")
|
||||
response = await client.get_response(message, tools=get_weather)
|
||||
print(f"Assistant: {response.text}")
|
||||
|
||||
# Streaming usage - same method, different parameter
|
||||
print(f"\nUser: {message}")
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(message, tools=get_weather, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
|
||||
# Example 2: Agent usage with single method
|
||||
agent = ChatAgent(
|
||||
chat_client=client,
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# Non-streaming agent
|
||||
print(f"\nUser: {message}")
|
||||
result = await agent.run(message, thread=thread) # default would be stream=False
|
||||
print(f"{agent.name}: {result.text}")
|
||||
|
||||
# Streaming agent - same method, different parameter
|
||||
print(f"\nUser: {message}")
|
||||
print(f"{agent.name}: ", end="")
|
||||
async for update in agent.run(message, thread=thread, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### .NET - Current pattern for comparison
|
||||
|
||||
```csharp
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You are good at telling jokes about pirates.",
|
||||
name: "PirateJoker");
|
||||
|
||||
// Non-streaming: Returns a string directly
|
||||
Console.WriteLine("=== Non-streaming ===");
|
||||
string result = await agent.RunAsync("Tell me a joke about a pirate.");
|
||||
Console.WriteLine(result);
|
||||
|
||||
// Streaming: Returns IAsyncEnumerable<AgentUpdate>
|
||||
Console.WriteLine("\n=== Streaming ===");
|
||||
await foreach (AgentUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
Console.WriteLine();
|
||||
|
||||
```
|
||||
@@ -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
|
||||