mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e39521fa08 | ||
|
|
b065a4ce51 | ||
|
|
38de991481 | ||
|
|
25696a72dc | ||
|
|
2cb78ea12e | ||
|
|
cee0a458fe | ||
|
|
4b9856e66f | ||
|
|
acaadc9c45 | ||
|
|
34329840e1 | ||
|
|
2a8c3e2dcf | ||
|
|
e43fc8ccec | ||
|
|
d992febe9b | ||
|
|
651e317907 | ||
|
|
1e527a328c | ||
|
|
e9727eb649 | ||
|
|
3a49b1d6dd | ||
|
|
a5eacbbe65 | ||
|
|
55b6e7a9f4 | ||
|
|
9c9d81d8b6 | ||
|
|
47a8a305d2 | ||
|
|
4e123a4d63 | ||
|
|
6e7254bba7 | ||
|
|
9c57680f00 | ||
|
|
7c2dae8855 | ||
|
|
3d09337446 | ||
|
|
3c727b5b71 | ||
|
|
35adfdb318 | ||
|
|
3f964c4cdb | ||
|
|
016daf3b98 | ||
|
|
f98d969d22 | ||
|
|
74c2ab8588 | ||
|
|
9db1bf8b61 | ||
|
|
9efecdd0e2 | ||
|
|
0f81c277d9 | ||
|
|
0e00e5f8dd | ||
|
|
31c866172a | ||
|
|
401e5dc7e8 | ||
|
|
18f7ba8632 | ||
|
|
05c53dce2d | ||
|
|
ade295b122 | ||
|
|
4527dee64b | ||
|
|
f45fc7d402 | ||
|
|
ca02146ee4 | ||
|
|
3627b9b911 | ||
|
|
b1b528e4a8 | ||
|
|
ca6cdd142e | ||
|
|
6b47cdbf52 | ||
|
|
cc0cfaaac8 | ||
|
|
3611be82cf | ||
|
|
0fcbe7e105 | ||
|
|
5530bc536b | ||
|
|
9bfa593ae7 | ||
|
|
3585581c7a | ||
|
|
63dee91a5f | ||
|
|
3b8b56e6ea | ||
|
|
9691c9c271 | ||
|
|
d2977d63da | ||
|
|
84fe6c46ab | ||
|
|
6626565f7a | ||
|
|
bfda595e56 | ||
|
|
efb14cedb1 | ||
|
|
dd3d085539 | ||
|
|
dc27740f1a | ||
|
|
c1435ac201 | ||
|
|
0bdcaa5c07 | ||
|
|
35f44e854e | ||
|
|
0756c45702 | ||
|
|
0b2ccd6126 | ||
|
|
23921c0f6e | ||
|
|
db16a9e74c | ||
|
|
c012aac5f2 | ||
|
|
49d69b3bf5 | ||
|
|
a9db40886e | ||
|
|
87962e53c5 | ||
|
|
5e056b672e | ||
|
|
4b533608b6 |
@@ -24,7 +24,9 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install Copilot CLI
|
||||
shell: bash
|
||||
@@ -32,7 +34,7 @@ runs:
|
||||
|
||||
- name: Test Copilot CLI
|
||||
shell: bash
|
||||
run: copilot -p "What can you do in one sentence?"
|
||||
run: copilot --version && copilot -p "What can you do in one sentence?"
|
||||
|
||||
- name: Azure CLI Login
|
||||
uses: azure/login@v2
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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"
|
||||
@@ -41,8 +41,7 @@ ENFORCED_TARGETS: set[str] = {
|
||||
"packages.purview.agent_framework_purview",
|
||||
"packages.anthropic.agent_framework_anthropic",
|
||||
"packages.azure-ai-search.agent_framework_azure_ai_search",
|
||||
"packages.core.agent_framework.azure",
|
||||
"packages.core.agent_framework.openai",
|
||||
"packages.openai.agent_framework_openai",
|
||||
# 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
|
||||
|
||||
@@ -60,9 +60,10 @@ jobs:
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_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:
|
||||
@@ -81,8 +82,8 @@ jobs:
|
||||
- name: Test with pytest (OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/core/tests/openai
|
||||
-m integration
|
||||
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
|
||||
@@ -96,7 +97,8 @@ jobs:
|
||||
env:
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -121,7 +123,9 @@ jobs:
|
||||
- name: Test with pytest (Azure OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/core/tests/azure
|
||||
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
|
||||
@@ -151,6 +155,13 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Start local MCP server
|
||||
id: local-mcp
|
||||
uses: ./.github/actions/setup-local-mcp-server
|
||||
with:
|
||||
fallback_url: ${{ env.LOCAL_MCP_URL }}
|
||||
- name: Prefer local MCP URL when available
|
||||
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
|
||||
- name: Test with pytest (Anthropic, Ollama, MCP integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
@@ -161,6 +172,26 @@ jobs:
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
- 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:
|
||||
@@ -170,12 +201,16 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_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_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ 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"
|
||||
@@ -209,18 +244,23 @@ jobs:
|
||||
packages/durabletask/tests/integration_tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
-x
|
||||
--timeout=360 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
|
||||
# Azure AI integration tests
|
||||
python-tests-azure-ai:
|
||||
name: Python Integration Tests - Azure AI
|
||||
# Foundry integration tests
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
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 }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -244,7 +284,13 @@ jobs:
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 15
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
|
||||
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
|
||||
|
||||
# Azure Cosmos integration tests
|
||||
python-tests-cosmos:
|
||||
@@ -301,7 +347,7 @@ jobs:
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-azure-ai,
|
||||
python-tests-foundry,
|
||||
python-tests-cosmos
|
||||
]
|
||||
steps:
|
||||
|
||||
@@ -47,6 +47,9 @@ jobs:
|
||||
filters: |
|
||||
python:
|
||||
- 'python/**'
|
||||
- '.github/actions/setup-local-mcp-server/**'
|
||||
- '.github/workflows/python-merge-tests.yml'
|
||||
- '.github/workflows/python-integration-tests.yml'
|
||||
core:
|
||||
- 'python/packages/core/agent_framework/_*.py'
|
||||
- 'python/packages/core/agent_framework/_workflows/**'
|
||||
@@ -54,20 +57,28 @@ jobs:
|
||||
- 'python/packages/core/agent_framework/observability.py'
|
||||
openai:
|
||||
- 'python/packages/core/agent_framework/openai/**'
|
||||
- 'python/packages/core/tests/openai/**'
|
||||
- 'python/packages/openai/**'
|
||||
- 'python/samples/**/providers/openai/**'
|
||||
azure:
|
||||
- 'python/packages/openai/**'
|
||||
- 'python/packages/core/agent_framework/azure/**'
|
||||
- 'python/packages/core/tests/azure/**'
|
||||
- 'python/samples/**/providers/azure/**'
|
||||
misc:
|
||||
- 'python/packages/anthropic/**'
|
||||
- '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/**'
|
||||
azure-ai:
|
||||
- 'python/packages/azure-ai/**'
|
||||
- 'python/packages/foundry/**'
|
||||
- 'python/samples/**/providers/foundry/**'
|
||||
cosmos:
|
||||
- 'python/packages/azure-cosmos/**'
|
||||
# run only if 'python' files were changed
|
||||
@@ -128,9 +139,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_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:
|
||||
@@ -146,8 +158,8 @@ jobs:
|
||||
- name: Test with pytest (OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/core/tests/openai
|
||||
-m integration
|
||||
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
|
||||
@@ -182,7 +194,8 @@ jobs:
|
||||
env:
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -205,7 +218,9 @@ jobs:
|
||||
- name: Test with pytest (Azure OpenAI integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/core/tests/azure
|
||||
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
|
||||
@@ -253,6 +268,13 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Start local MCP server
|
||||
id: local-mcp
|
||||
uses: ./.github/actions/setup-local-mcp-server
|
||||
with:
|
||||
fallback_url: ${{ env.LOCAL_MCP_URL }}
|
||||
- name: Prefer local MCP URL when available
|
||||
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
|
||||
- name: Test with pytest (Anthropic, Ollama, MCP integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
@@ -264,6 +286,26 @@ jobs:
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
working-directory: ./python
|
||||
- name: Stop local MCP server
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
server_pid="${{ steps.local-mcp.outputs.pid }}"
|
||||
if [[ -z "$server_pid" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" 2>/dev/null || true
|
||||
for _ in $(seq 1 10); do
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@v0.7.2
|
||||
@@ -288,12 +330,16 @@ jobs:
|
||||
environment: integration
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_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_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ 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"
|
||||
@@ -325,7 +371,8 @@ jobs:
|
||||
packages/durabletask/tests/integration_tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
-x
|
||||
--timeout=360 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
working-directory: ./python
|
||||
- name: Surface failing tests
|
||||
@@ -338,8 +385,8 @@ jobs:
|
||||
fail-on-empty: false
|
||||
title: Functions integration test results
|
||||
|
||||
python-tests-azure-ai:
|
||||
name: Python Tests - Azure AI
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
needs: paths-filter
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
@@ -352,6 +399,10 @@ jobs:
|
||||
env:
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
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 }}
|
||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -373,7 +424,13 @@ jobs:
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 15
|
||||
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
|
||||
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
|
||||
working-directory: ./python
|
||||
- name: Test Azure AI samples
|
||||
timeout-minutes: 10
|
||||
@@ -460,7 +517,7 @@ jobs:
|
||||
python-tests-azure-openai,
|
||||
python-tests-misc-integration,
|
||||
python-tests-functions,
|
||||
python-tests-azure-ai,
|
||||
python-tests-foundry,
|
||||
python-tests-cosmos,
|
||||
]
|
||||
steps:
|
||||
|
||||
@@ -23,10 +23,8 @@ jobs:
|
||||
environment: integration
|
||||
env:
|
||||
# Required configuration for get-started samples
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
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
|
||||
@@ -41,6 +39,11 @@ jobs:
|
||||
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
|
||||
@@ -50,24 +53,27 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-01-get-started
|
||||
path: python/scripts/sample_validation/reports/
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents:
|
||||
name: Validate 02-agents
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Azure AI configuration
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# 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_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||
# OpenAI configuration
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
# GitHub MCP
|
||||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
# Observability
|
||||
ENABLE_INSTRUMENTATION: "true"
|
||||
defaults:
|
||||
@@ -84,29 +90,150 @@ jobs:
|
||||
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_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
|
||||
echo "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=$AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME" >> .env
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||
echo "GITHUB_PAT=$GITHUB_PAT" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents --save-report --report-name 02-agents
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers --save-report --report-name 02-agents
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents
|
||||
path: python/scripts/sample_validation/reports/
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-03-workflows:
|
||||
name: Validate 03-workflows
|
||||
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_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_MODEL=$OPENAI_MODEL" >> .env
|
||||
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-openai
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-azure:
|
||||
name: Validate 02-agents/providers/azure
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Azure AI configuration
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||
echo "AZURE_OPENAI_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
|
||||
echo "AZURE_OPENAI_API_VERSION=$AZURE_OPENAI_API_VERSION" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-azure
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-anthropic:
|
||||
name: Validate 02-agents/providers/anthropic
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env
|
||||
echo "ANTHROPIC_CHAT_MODEL_ID=$ANTHROPIC_CHAT_MODEL_ID" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-anthropic
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-github-copilot:
|
||||
name: Validate 02-agents/providers/github_copilot
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
@@ -121,6 +248,220 @@ jobs:
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-github-copilot
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-amazon:
|
||||
name: Validate 02-agents/providers/amazon
|
||||
if: false # Temporarily disabled - requires AWS credentials
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
BEDROCK_CHAT_MODEL_ID: ${{ vars.BEDROCK__CHATMODELID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-amazon
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-ollama:
|
||||
name: Validate 02-agents/providers/ollama
|
||||
if: false # Temporarily disabled - requires local Ollama server
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
OLLAMA_MODEL: ${{ vars.OLLAMA__MODEL }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-ollama
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-foundry:
|
||||
name: Validate 02-agents/providers/foundry
|
||||
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME || '' }}
|
||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
||||
echo "FOUNDRY_AGENT_NAME=$FOUNDRY_AGENT_NAME" >> .env
|
||||
echo "FOUNDRY_AGENT_VERSION=$FOUNDRY_AGENT_VERSION" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-foundry
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-copilotstudio:
|
||||
name: Validate 02-agents/providers/copilotstudio
|
||||
if: false # Temporarily disabled - requires Copilot Studio setup
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
||||
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
||||
COPILOTSTUDIOAGENT__TENANTID: ${{ secrets.COPILOTSTUDIOAGENT__TENANTID }}
|
||||
COPILOTSTUDIOAGENT__AGENTAPPID: ${{ secrets.COPILOTSTUDIOAGENT__AGENTAPPID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
|
||||
echo "COPILOTSTUDIOAGENT__AGENTAPPID=$COPILOTSTUDIOAGENT__AGENTAPPID" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-copilotstudio
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-02-agents-custom:
|
||||
name: Validate 02-agents/providers/custom
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents-custom
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-03-workflows:
|
||||
name: Validate 03-workflows
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/sample-validation-setup
|
||||
with:
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
os: ${{ runner.os }}
|
||||
|
||||
- name: Create .env for samples
|
||||
run: |
|
||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
|
||||
@@ -130,20 +471,16 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-03-workflows
|
||||
path: python/scripts/sample_validation/reports/
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-04-hosting:
|
||||
name: Validate 04-hosting
|
||||
if: false # Temporarily disabled because of sample complexity
|
||||
if: false # Temporarily disabled because of sample complexity
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Azure AI configuration
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# Azure OpenAI configuration
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
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:
|
||||
@@ -169,27 +506,26 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-04-hosting
|
||||
path: python/scripts/sample_validation/reports/
|
||||
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
|
||||
if: false # Temporarily disabled because of sample complexity
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Azure AI configuration
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
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_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ 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
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME_WORKFLOW: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
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
|
||||
@@ -213,23 +549,23 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-05-end-to-end
|
||||
path: python/scripts/sample_validation/reports/
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-autogen-migration:
|
||||
name: Validate autogen-migration
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Azure AI configuration
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
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_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# OpenAI configuration
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
@@ -244,6 +580,16 @@ jobs:
|
||||
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_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||
|
||||
- name: Run sample validation
|
||||
run: |
|
||||
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
|
||||
@@ -253,24 +599,23 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-autogen-migration
|
||||
path: python/scripts/sample_validation/reports/
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
validate-semantic-kernel-migration:
|
||||
name: Validate semantic-kernel-migration
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
env:
|
||||
# Azure AI configuration
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
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_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||
# OpenAI configuration
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||
# Copilot Studio
|
||||
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
||||
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
||||
@@ -290,6 +635,20 @@ jobs:
|
||||
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_DEPLOYMENT_NAME=$AZURE_OPENAI_DEPLOYMENT_NAME" >> .env
|
||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .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
|
||||
@@ -299,4 +658,67 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-semantic-kernel-migration
|
||||
path: python/scripts/sample_validation/reports/
|
||||
path: python/samples/sample_validation/reports/
|
||||
|
||||
aggregate-results:
|
||||
name: Aggregate Results
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- validate-01-get-started
|
||||
- validate-02-agents
|
||||
- validate-02-agents-openai
|
||||
- validate-02-agents-azure
|
||||
- validate-02-agents-anthropic
|
||||
- validate-02-agents-github-copilot
|
||||
- validate-02-agents-amazon
|
||||
- validate-02-agents-ollama
|
||||
- validate-02-agents-foundry
|
||||
- validate-02-agents-copilotstudio
|
||||
- validate-02-agents-custom
|
||||
- validate-03-workflows
|
||||
- validate-04-hosting
|
||||
- validate-05-end-to-end
|
||||
- validate-autogen-migration
|
||||
- validate-semantic-kernel-migration
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download all validation reports
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: validation-report-*
|
||||
path: reports/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Restore validation history
|
||||
id: cache-restore
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
validation-history-
|
||||
|
||||
- name: Aggregate results and generate trend report
|
||||
run: |
|
||||
python3 python/scripts/sample_validation/aggregate.py \
|
||||
reports/ \
|
||||
validation-history/history.json \
|
||||
trend-report.md
|
||||
|
||||
- name: Write trend report to job summary
|
||||
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save validation history
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
|
||||
- name: Upload trend report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: validation-trend-report
|
||||
path: trend-report.md
|
||||
|
||||
+47
-8
@@ -74,6 +74,37 @@ 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:
|
||||
@@ -92,22 +123,30 @@ 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-scripts) to ensure that your build is clean and all tests are passing.
|
||||
6. Run the relevant scripts in [the section below](#development-setup) 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 scripts
|
||||
### Development Setup
|
||||
|
||||
The scripts below are used to build, test, and lint within the project.
|
||||
Each language has its own dev setup guide, coding standards, and build scripts:
|
||||
|
||||
- Python: see [python/DEV_SETUP.md](./python/DEV_SETUP.md).
|
||||
- .NET:
|
||||
- Build: `dotnet build`
|
||||
- Test: `dotnet test`
|
||||
- Linting (auto-fix): `dotnet format`
|
||||
- **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`
|
||||
|
||||
### PR - CI Process
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Welcome to Microsoft Agent Framework!
|
||||
|
||||
[](https://discord.gg/b5zjErwbQM)
|
||||
[](https://discord.gg/b5zjErwbQM)
|
||||
[](https://learn.microsoft.com/en-us/agent-framework/)
|
||||
[](https://pypi.org/project/agent-framework/)
|
||||
[](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
|
||||
@@ -94,23 +94,23 @@ Create a simple Azure Responses Agent that writes a haiku about the Microsoft Ag
|
||||
# Use `az login` to authenticate with Azure CLI
|
||||
import os
|
||||
import asyncio
|
||||
from agent_framework.azure import AzureOpenAIResponsesClient
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize a chat agent with Azure OpenAI Responses
|
||||
# Initialize a chat agent with Microsoft Foundry
|
||||
# the endpoint, deployment name, and api version can be set via environment variables
|
||||
# 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
|
||||
).as_agent(
|
||||
name="HaikuBot",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
# or they can be passed in directly to the FoundryChatClient constructor
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
credential=AzureCliCredential(),
|
||||
# project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
# model=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"],
|
||||
),
|
||||
name="HaikuBot",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
)
|
||||
|
||||
print(await agent.run("Write a haiku about Microsoft Agent Framework."))
|
||||
@@ -137,24 +137,21 @@ var agent = new OpenAIClient("<apikey>")
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
// dotnet add package Microsoft.Agents.AI.AzureAI --prerelease
|
||||
// dotnet add package Azure.Identity
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using System.ClientModel.Primitives;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// 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") })
|
||||
.GetResponsesClient("gpt-4o-mini")
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
@@ -163,15 +160,43 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
||||
|
||||
### Python
|
||||
|
||||
- [Getting Started with Agents](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
|
||||
- [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.)
|
||||
- [Getting Started with Workflows](./python/samples/03-workflows): workflow creation and integration with agents
|
||||
- [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
|
||||
|
||||
### .NET
|
||||
|
||||
- [Getting Started with Agents](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
|
||||
- [Agent Provider Samples](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
|
||||
- [Workflow Samples](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
|
||||
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
|
||||
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
|
||||
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
|
||||
- [Workflows](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
|
||||
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
|
||||
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Authentication errors when using Azure credentials | Not signed in to Azure CLI | Run `az login` before starting your app |
|
||||
| API key errors | Wrong or missing API key | Verify the key and ensure it's for the correct resource/provider |
|
||||
|
||||
> **Tip:** `DefaultAzureCredential` is convenient for development but in production, consider using a specific credential (e.g., `ManagedIdentityCredential`) to avoid latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The samples typically read configuration from environment variables. Common required variables:
|
||||
|
||||
| Variable | Used by | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI samples | Your Azure OpenAI resource URL |
|
||||
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Azure OpenAI samples | Model deployment name (e.g. `gpt-4o-mini`) |
|
||||
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry samples | Your Microsoft Foundry project endpoint |
|
||||
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Microsoft Foundry samples | Model deployment name |
|
||||
| `OPENAI_API_KEY` | OpenAI (non-Azure) samples | Your OpenAI platform API key |
|
||||
|
||||
## Contributor Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: rogerbarreto
|
||||
date: 2026-03-06
|
||||
deciders: rogerbarreto, alliscode
|
||||
consulted: ""
|
||||
informed: ""
|
||||
---
|
||||
|
||||
# Foundry agent surface stays centered on `ChatClientAgent`
|
||||
|
||||
## Context
|
||||
|
||||
The Microsoft Foundry integration exposes two distinct usage patterns:
|
||||
|
||||
1. Direct Responses usage, where callers provide model, instructions, and tools at runtime.
|
||||
2. Server-side versioned agents, where callers create and manage `AgentVersion` resources through `AIProjectClient.Agents`.
|
||||
|
||||
We briefly explored adding public wrapper types such as `FoundryAgent`, `FoundryVersionedAgent`, and `FoundryResponsesChatClient` to make those paths feel more specialized. That direction created extra public types, duplicated existing `ChatClientAgent` behavior, and pushed samples toward compatibility helpers instead of the native Azure SDK flow.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the public surface centered on `ChatClientAgent`.
|
||||
|
||||
- Direct Responses scenarios use `AIProjectClient.AsAIAgent(...)`.
|
||||
- Server-side versioned scenarios use native `AIProjectClient.Agents` APIs to create or retrieve agent resources, then wrap `AgentRecord` or `AgentVersion` with `AIProjectClient.AsAIAgent(...)`.
|
||||
- Compatibility helpers such as `AIProjectClient.CreateAIAgentAsync(...)` and `AIProjectClient.GetAIAgentAsync(...)` remain only as obsolete migration shims.
|
||||
- Public wrapper types `FoundryAgent`, `FoundryVersionedAgent`, `FoundryResponsesChatClient`, and `FoundryResponsesChatClientAgent` are not part of the chosen direction.
|
||||
|
||||
## Why
|
||||
|
||||
- `ChatClientAgent` is already the framework abstraction used everywhere else.
|
||||
- `AIProjectClient` is the native Azure SDK entry point for versioned agent lifecycle operations.
|
||||
- A single agent abstraction avoids parallel type hierarchies for the same backend.
|
||||
- Samples become clearer when they show either:
|
||||
- direct Responses construction via `AIProjectClient.AsAIAgent(...)`, or
|
||||
- native Foundry resource management via `AIProjectClient.Agents`.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Direct Responses path
|
||||
|
||||
Use the convenience overloads on `AIProjectClient`:
|
||||
|
||||
```csharp
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
ChatClientAgent agent = aiProjectClient.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You are good at telling jokes.",
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
Or use composed `ChatClientAgent`
|
||||
|
||||
```csharp
|
||||
ProjectResponsesClient projectResponsesClient = new(new Uri(endpoint), new DefaultAzureCredential(), new AgentReference($"model:{deploymentName}"));
|
||||
|
||||
ChatClientAgent agent = new(
|
||||
chatClient: projectResponsesClient.AsIChatClient(),
|
||||
instructions: "You are good at telling jokes.",
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
This path is code-first and does not create a persistent server-side agent.
|
||||
|
||||
### Versioned agent path
|
||||
|
||||
Use the convenience overloads on `AIProjectClient`:
|
||||
|
||||
```csharp
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
"JokerAgent",
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes."
|
||||
}));
|
||||
|
||||
ChatClientAgent agent = aiProjectClient.AsAIAgent(version);
|
||||
```
|
||||
|
||||
Or use composed `ChatClientAgent`
|
||||
|
||||
```csharp
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
"JokerAgent",
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes."
|
||||
}));
|
||||
|
||||
ProjectResponsesClient projectResponsesClient = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
.GetProjectResponsesClientForAgent(new AgentReference(version.Name, version.Version));
|
||||
|
||||
ChatClientAgent agent = new(
|
||||
chatClient: projectResponsesClient.AsIChatClient(),
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
### Samples
|
||||
|
||||
- `FoundryAgents/` samples show the direct Responses path with `AIProjectClient.AsAIAgent(...)`.
|
||||
- `FoundryVersionedAgents/` samples should show native `AIProjectClient.Agents` create/get/delete flows plus `AsAIAgent(...)`.
|
||||
|
||||
### Compatibility APIs
|
||||
|
||||
Obsolete helper extensions remain only to ease migration of existing code. New samples and new guidance should not be written against them.
|
||||
|
||||
## Rejected direction
|
||||
|
||||
Do not introduce or preserve separate public wrapper types whose main purpose is to forward to `ChatClientAgent` while carrying Foundry-specific naming.
|
||||
|
||||
That approach:
|
||||
|
||||
- duplicates lifecycle concepts already present on `AIProjectClient`,
|
||||
- fragments the public API,
|
||||
- complicates samples and docs,
|
||||
- and makes migration harder by encouraging wrapper-specific affordances.
|
||||
@@ -0,0 +1,960 @@
|
||||
status: proposed
|
||||
date: 2026-03-23
|
||||
contact: sergeymenshykh
|
||||
deciders: rbarreto, westey-m, eavanvalkenburg
|
||||
---
|
||||
|
||||
# Agent Skills: Multi-Source Architecture
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The Agent Framework needs a skills system that lets agents discover and use domain-specific knowledge, reference documents, and executable scripts. Skills can originate from different sources — filesystem directories (SKILL.md files), inline C# code, or reusable class libraries — and the framework must support all three uniformly while allowing extensibility, composition, and filtering.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Skills must be definable from multiple sources: filesystem, inline code, reusable classes, etc
|
||||
- Common abstractions are needed so the provider and builder work uniformly regardless of skill origin
|
||||
- File-based scripts must support user-defined executors, enabling custom runtimes and languages; code/class-based scripts execute in-process as C# delegates
|
||||
- Skills must be filterable so consumers can include or exclude specific skills based on defined criteria
|
||||
- Multiple skill sources must be composable into a single provider
|
||||
- It must be possible to add custom skill sources (e.g., databases, REST APIs, package registries) by implementing a common abstraction
|
||||
|
||||
## Architecture
|
||||
|
||||
### Model-Facing Tools
|
||||
|
||||
Skills are presented to the model as up to three tools that progressively disclose skill content. The system prompt lists available skill names and descriptions; the model then calls these tools on demand:
|
||||
|
||||
- **`load_skill(skillName)`** — returns the full skill body (instructions, listed resources, listed scripts)
|
||||
- **`read_skill_resource(skillName, resourceName)`** — reads a supplementary resource (file-based or code-defined) associated with a skill
|
||||
- **`run_skill_script(skillName, scriptName, arguments?)`** — executes a script associated with a skill; only registered when at least one skill contains scripts
|
||||
|
||||
Each tool delegates to the corresponding method on the resolved `AgentSkill` — calling `Resource.ReadAsync()` or `Script.RunAsync()` respectively.
|
||||
|
||||
If skills have no scripts defined, the `run_skill_script` tool is **not advertised** to the model and instructions related to script execution are **not included** in the default skills instructions.
|
||||
|
||||
### Abstract Base Types
|
||||
|
||||
The architecture defines four abstract base types that all skill variants implement:
|
||||
|
||||
```csharp
|
||||
public abstract class AgentSkill
|
||||
{
|
||||
public abstract AgentSkillFrontmatter Frontmatter { get; }
|
||||
public abstract string Content { get; }
|
||||
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
|
||||
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
||||
}
|
||||
|
||||
public abstract class AgentSkillResource
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public abstract class AgentSkillScript
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public abstract class AgentSkillsSource
|
||||
{
|
||||
public abstract Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
Skill metadata is captured via `AgentSkillFrontmatter`:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentSkillFrontmatter
|
||||
{
|
||||
public AgentSkillFrontmatter(string name, string description) { ... }
|
||||
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public string? License { get; set; }
|
||||
public string? Compatibility { get; set; }
|
||||
public string? AllowedTools { get; set; }
|
||||
public AdditionalPropertiesDictionary? Metadata { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
The type hierarchy at a glance:
|
||||
|
||||
```
|
||||
AgentSkill (abstract) AgentSkillsSource (abstract)
|
||||
├── AgentFileSkill ├── AgentFileSkillsSource (public)
|
||||
└── [Programmatic] ├── AgentInMemorySkillsSource (public)
|
||||
├── AgentInlineSkill ├── AggregatingAgentSkillsSource (public)
|
||||
└── AgentClassSkill (abstract) └── DelegatingAgentSkillsSource (abstract, public)
|
||||
├── FilteringAgentSkillsSource (public)
|
||||
AgentSkillResource (abstract) ├── CachingAgentSkillsSource (public)
|
||||
├── AgentFileSkillResource └── DeduplicatingAgentSkillsSource (public)
|
||||
└── AgentInlineSkillResource
|
||||
AgentSkillScript (abstract)
|
||||
├── AgentFileSkillScript
|
||||
└── AgentInlineSkillScript
|
||||
```
|
||||
|
||||
There are two top-level categories of skills:
|
||||
|
||||
1. **File-Based Skills** — discovered from `SKILL.md` files on the filesystem. Resources and scripts are files in subdirectories.
|
||||
2. **Programmatic Skills** — defined in C# code. These are further divided into:
|
||||
- **Inline Skills** — built at runtime via the `AgentInlineSkill` class and its fluent API. Ideal for quick, agent-specific skill definitions.
|
||||
- **Class-Based Skills** — defined as reusable C# classes that subclass `AgentClassSkill`. Ideal for packaging skills as shared libraries or NuGet packages.
|
||||
|
||||
Both programmatic skill types use `AgentInlineSkillResource` and `AgentInlineSkillScript` for their resources and scripts. They are typically served by `AgentInMemorySkillsSource`, which accepts any `AgentSkill` and is not limited to programmatic skills.
|
||||
|
||||
### File-Based Skills
|
||||
|
||||
File-based skills are authored as `SKILL.md` files on disk. Resources and scripts are discovered from corresponding subfolders within the skill directory.
|
||||
|
||||
**`AgentFileSkill`** — A filesystem-based skill discovered from a directory containing a `SKILL.md` file. Parsed from YAML frontmatter; content is the raw markdown body. Resources and scripts are discovered from files in corresponding subfolders:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentFileSkill : AgentSkill
|
||||
{
|
||||
internal AgentFileSkill(
|
||||
AgentSkillFrontmatter frontmatter, string content, string path,
|
||||
IReadOnlyList<AgentSkillResource>? resources = null,
|
||||
IReadOnlyList<AgentSkillScript>? scripts = null) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentFileSkillResource`** — A file-based skill resource. Reads content from a file on disk relative to the skill directory:
|
||||
|
||||
```csharp
|
||||
internal sealed class AgentFileSkillResource : AgentSkillResource
|
||||
{
|
||||
public AgentFileSkillResource(string name, string fullPath) { ... }
|
||||
|
||||
public string FullPath { get; }
|
||||
|
||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentFileSkillScript`** — A file-based skill script that represents a script file on disk. Delegates execution to an external `AgentFileSkillScriptRunner` callback (e.g., runs Python/shell via `Process.Start`). Throws `NotSupportedException` if no executor is configured:
|
||||
|
||||
```csharp
|
||||
public delegate Task<object?> AgentFileSkillScriptRunner(
|
||||
AgentFileSkill skill, AgentFileSkillScript script,
|
||||
AIFunctionArguments arguments, CancellationToken cancellationToken);
|
||||
|
||||
public sealed class AgentFileSkillScript : AgentSkillScript
|
||||
{
|
||||
private readonly AgentFileSkillScriptRunner _executor;
|
||||
|
||||
internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
|
||||
: base(name) { ... }
|
||||
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
|
||||
{
|
||||
|
||||
return await _executor(fileSkill, this, arguments, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The executor can be provided at the **provider level** via `AgentSkillsProviderBuilder.UseFileScriptRunner(executor)` and optionally overridden for a **particular file skill** or for a **set of skills** at the file skill source level, giving fine-grained control over how different scripts are executed.
|
||||
|
||||
**`AgentFileSkillsSource`** — A skill source that discovers skills from filesystem directories containing `SKILL.md` files. Recursively scans directories (max 2 levels), validates frontmatter, and enforces path traversal and symlink security checks:
|
||||
|
||||
```csharp
|
||||
public sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
||||
{
|
||||
public AgentFileSkillsSource(
|
||||
IEnumerable<string> skillPaths,
|
||||
AgentFileSkillScriptRunner scriptRunner,
|
||||
AgentFileSkillsSourceOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentFileSkillsSourceOptions`** — Configuration options for `AgentFileSkillsSource`. Allows customizing the allowed file extensions for resources and scripts without adding constructor parameters:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentFileSkillsSourceOptions
|
||||
{
|
||||
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
|
||||
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Example** — A file-based skill on disk and how it is added to a source:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── unit-converter/
|
||||
├── SKILL.md # frontmatter + instructions
|
||||
├── resources/
|
||||
│ └── conversion-table.csv # discovered as a resource
|
||||
└── scripts/
|
||||
└── convert.py # discovered as a script
|
||||
```
|
||||
|
||||
```csharp
|
||||
var source = new AgentFileSkillsSource(skillPaths: ["./skills"], scriptRunner: SubprocessScriptRunner.RunAsync);
|
||||
|
||||
var provider = new AgentSkillsProvider(source);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
### Programmatic Skills
|
||||
|
||||
Programmatic skills are defined in C# code rather than discovered from the filesystem. There are two kinds: **inline** and **class-based**. Both use `AgentInlineSkillResource` and `AgentInlineSkillScript` for resources and scripts, and are held by a single `AgentInMemorySkillsSource`.
|
||||
|
||||
**`AgentInMemorySkillsSource`** — A general-purpose skill source that holds any `AgentSkill` instances in memory. Although commonly used for programmatic skills (`AgentInlineSkill` and `AgentClassSkill`), it accepts any `AgentSkill` subclass and is not restricted to code-defined skills:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInMemorySkillsSource : AgentSkillsSource
|
||||
{
|
||||
public AgentInMemorySkillsSource(
|
||||
IEnumerable<AgentSkill> skills,
|
||||
ILoggerFactory? loggerFactory = null) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### Inline Skills
|
||||
|
||||
Inline skills are built at runtime via the `AgentInlineSkill` class and its fluent API. They are ideal for quick, agent-specific skill definitions where a full class hierarchy would be overkill.
|
||||
|
||||
**`AgentInlineSkill`** — A skill defined entirely in code. Resources can be static values or functions; scripts are always functions. Constructed with name, description, and instructions, then extended with resources and scripts:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkill : AgentSkill
|
||||
{
|
||||
public AgentInlineSkill(string name, string description, string instructions, string? license = null, string? compatibility = null, ...) { ... }
|
||||
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions) { ... }
|
||||
|
||||
public AgentInlineSkill AddResource(object value, string name, string? description = null);
|
||||
public AgentInlineSkill AddResource(Delegate handler, string name, string? description = null);
|
||||
public AgentInlineSkill AddScript(Delegate handler, string name, string? description = null);
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentInlineSkillResource`** — A skill resource that wraps a static value:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkillResource : AgentSkillResource
|
||||
{
|
||||
public AgentInlineSkillResource(object value, string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<object?>(_value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentInlineSkillResource`** — A skill resource backed by a delegate. The delegate is invoked via an `AIFunction` each time `ReadAsync` is called, producing a dynamic (computed) value:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkillResource : AgentSkillResource
|
||||
{
|
||||
public AgentInlineSkillResource(Delegate handler, string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
_function = AIFunctionFactory.Create(handler, name: name);
|
||||
}
|
||||
|
||||
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _function.InvokeAsync(new AIFunctionArguments() { Services = serviceProvider }, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AgentInlineSkillScript`** — A skill script backed by a delegate via an `AIFunction`:
|
||||
|
||||
```csharp
|
||||
public sealed class AgentInlineSkillScript : AgentSkillScript
|
||||
{
|
||||
private readonly AIFunction _function;
|
||||
|
||||
public AgentInlineSkillScript(Delegate handler, string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
_function = AIFunctionFactory.Create(handler, name: name);
|
||||
}
|
||||
|
||||
public JsonElement? ParametersSchema => _function.JsonSchema;
|
||||
|
||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
|
||||
{
|
||||
return await _function.InvokeAsync(arguments, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example** — Creating an inline skill with a resource and script, then adding it to a source:
|
||||
|
||||
```csharp
|
||||
var skill = new AgentInlineSkill(
|
||||
name: "unit-converter",
|
||||
description: "Converts between measurement units.",
|
||||
instructions: """
|
||||
Use this skill to convert values between metric and imperial units.
|
||||
Refer to the conversion-table resource for supported unit pairs.
|
||||
Run the convert script to perform conversions.
|
||||
"""
|
||||
)
|
||||
.AddResource("kg=2.205lb, m=3.281ft, L=0.264gal", "conversion-table", "Supported unit pairs")
|
||||
.AddScript(Convert, "convert", "Converts a value between units");
|
||||
|
||||
var source = new AgentInMemorySkillsSource([skill]);
|
||||
|
||||
var provider = new AgentSkillsProvider(source);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
|
||||
static string Convert(double value, double factor)
|
||||
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
|
||||
```
|
||||
|
||||
#### Class-Based Skills
|
||||
|
||||
Class-based skills are designed for packaging skills as reusable libraries. Users subclass `AgentClassSkill` and override properties. Unlike inline skills, class-based skills are self-contained, can live in shared libraries or NuGet packages, and are well-suited for dependency injection.
|
||||
|
||||
**`AgentClassSkill`** — An abstract base class for defining skills as reusable C# classes that bundle all skill components (frontmatter, instructions, resources, scripts) together. Designed for packaging skills as distributable libraries:
|
||||
|
||||
```csharp
|
||||
public abstract class AgentClassSkill : AgentSkill
|
||||
{
|
||||
public abstract string Instructions { get; }
|
||||
|
||||
// Content is auto-synthesized from Frontmatter + Instructions + Resources + Scripts
|
||||
public override string Content =>
|
||||
SkillContentBuilder.BuildContent(Frontmatter.Name, Frontmatter.Description,
|
||||
SkillContentBuilder.BuildBody(Instructions, Resources, Scripts));
|
||||
}
|
||||
```
|
||||
|
||||
**Example** — Defining a class-based skill and adding it to a source:
|
||||
|
||||
```csharp
|
||||
public class UnitConverterSkill : AgentClassSkill
|
||||
{
|
||||
public override AgentSkillFrontmatter Frontmatter { get; } =
|
||||
new("unit-converter", "Converts between measurement units.");
|
||||
|
||||
public override string Instructions => """
|
||||
Use this skill to convert values between metric and imperial units.
|
||||
Refer to the conversion-table resource for supported unit pairs.
|
||||
Run the convert script to perform conversions.
|
||||
""";
|
||||
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
|
||||
[
|
||||
new AgentInlineSkillResource("kg=2.205lb, m=3.281ft", "conversion-table"),
|
||||
];
|
||||
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
|
||||
[
|
||||
new AgentInlineSkillScript(Convert, "convert"),
|
||||
];
|
||||
|
||||
private static string Convert(double value, double factor)
|
||||
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
|
||||
}
|
||||
|
||||
var source = new AgentInMemorySkillsSource([new UnitConverterSkill()]);
|
||||
|
||||
var provider = new AgentSkillsProvider(source);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
## Filtering, Caching, and Deduplication
|
||||
|
||||
The following subsections present alternative approaches for handling filtering, caching, and deduplication of skills across multiple sources.
|
||||
|
||||
### Via Composition
|
||||
|
||||
In this approach, the `AgentSkillsProvider` accepts a **single** `AgentSkillsSource`. Multiple sources are composed externally via an aggregate source, and cross-cutting concerns like filtering, caching, and deduplication are implemented as **source decorators** — subclasses of `DelegatingAgentSkillsSource` that intercept `GetSkillsAsync()`.
|
||||
|
||||
**`FilteringAgentSkillsSource`** — A decorator that applies filter logic before returning results. The decorator pattern keeps filtering orthogonal to source implementations and allows composing multiple filters:
|
||||
|
||||
```csharp
|
||||
public sealed class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
|
||||
{
|
||||
private readonly Func<AgentSkill, bool> _predicate;
|
||||
|
||||
public FilteringAgentSkillsSource(AgentSkillsSource innerSource, Func<AgentSkill, bool> predicate)
|
||||
: base(innerSource)
|
||||
{
|
||||
_predicate = predicate;
|
||||
}
|
||||
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var skills = await this.InnerSource.GetSkillsAsync(cancellationToken);
|
||||
return skills.Where(_predicate).ToList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`CachingAgentSkillsSource`** — A decorator that caches skills after the first load, keeping the provider stateless and giving consumers control over caching granularity per source. For example, file-based skills (expensive to discover) can be cached while code-defined skills remain uncached:
|
||||
|
||||
```csharp
|
||||
public sealed class CachingAgentSkillsSource : DelegatingAgentSkillsSource
|
||||
{
|
||||
private IList<AgentSkill>? _cached;
|
||||
|
||||
public CachingAgentSkillsSource(AgentSkillsSource innerSource)
|
||||
: base(innerSource)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _cached ??= await this.InnerSource.GetSkillsAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deduplication** is similarly implemented as a decorator (`DeduplicatingAgentSkillsSource`) that deduplicates by name (case-insensitive, first-one-wins) and logs a warning for skipped duplicates.
|
||||
|
||||
**Example** — Combining file-based and code-defined sources with filtering and caching:
|
||||
|
||||
```csharp
|
||||
var fileSource = new CachingAgentSkillsSource(new AgentFileSkillsSource(["./skills"]));
|
||||
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
|
||||
|
||||
var compositeSource = new FilteringAgentSkillsSource(
|
||||
new AggregatingAgentSkillsSource([fileSource, codeSource]),
|
||||
filter: s => s.Frontmatter.Name != "internal");
|
||||
|
||||
var provider = new AgentSkillsProvider(compositeSource);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Clean single-responsibility: the provider serves skills, sources provide them.
|
||||
- Caching, filtering, and deduplication are composable as source decorators — each concern is a separate, testable wrapper.
|
||||
|
||||
**Cons:**
|
||||
- DI is less flexible: multiple `AgentSkillsSource` implementations registered in the container cannot be auto-injected into the provider. The consumer must manually compose them via an aggregate source.
|
||||
- Increased public API surface: requires additional public classes (aggregate source, caching decorators, filtering decorators) that consumers need to learn and use.
|
||||
|
||||
### Via AgentSkillsProvider
|
||||
|
||||
In this approach, the `AgentSkillsProvider` accepts **`IEnumerable<AgentSkillsSource>`** and handles aggregation, filtering, caching, and deduplication internally.
|
||||
|
||||
The provider aggregates skills from all registered sources, deduplicates by name (case-insensitive, first-one-wins), caches the result after the first load, and optionally applies filtering via a predicate on `AgentSkillsProviderOptions`. Duplicate skill names are logged as warnings.
|
||||
|
||||
**Example** — Registering multiple sources directly with the provider:
|
||||
|
||||
```csharp
|
||||
// Conceptual example — in practice, use AgentSkillsProviderBuilder
|
||||
var fileSource = new AgentFileSkillsSource(["./skills"]);
|
||||
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
|
||||
|
||||
var provider = new AgentSkillsProvider(
|
||||
sources: [fileSource, codeSource],
|
||||
options: new AgentSkillsProviderOptions
|
||||
{
|
||||
Filter = s => s.Frontmatter.Name != "internal",
|
||||
});
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- DI-friendly: register multiple `AgentSkillsSource` implementations in the container, and they are all auto-injected into `AgentSkillsProvider` via `IEnumerable<AgentSkillsSource>`.
|
||||
- Smaller public API surface: no need for aggregate source, caching decorators, or filtering decorator classes — these concerns are handled internally by the provider.
|
||||
|
||||
**Cons:**
|
||||
- The provider takes on multiple responsibilities — aggregation, caching, deduplication, and filtering.
|
||||
- Less granular caching control: caching is all-or-nothing across sources rather than per-source as with decorators.
|
||||
- Less extensible: new behaviors (e.g., ordering, TTL expiration) require modifying the provider rather than adding a decorator.
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
**`AgentSkillsProviderBuilder`** provides a fluent API for composing skills from multiple sources. The builder centralizes configuration — script executors, approval callbacks, prompt templates, and filtering — so consumers don't need to know the underlying source types.
|
||||
|
||||
The builder internally decides how to wire up the object graph: it creates the appropriate source instances, applies caching and filtering, and returns a fully configured `AgentSkillsProvider`. This keeps the setup code concise while still allowing fine-grained control when needed.
|
||||
|
||||
**Example** — Using the builder to combine multiple source types with configuration:
|
||||
|
||||
```csharp
|
||||
var provider = new AgentSkillsProviderBuilder()
|
||||
.UseFileSkill("./skills") // file-based source
|
||||
.UseInlineSkills(codeSkill) // code-defined source
|
||||
.UseClassSkills(new ClassSkill()) // class-based source
|
||||
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync) // script runner
|
||||
.UseScriptApproval() // optional human-in-the-loop
|
||||
.UsePromptTemplate(customTemplate) // optional prompt customization
|
||||
.UseFilter(s => s.Frontmatter.Name != "internal") // optional skill filtering
|
||||
.Build();
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
AIContextProviders = [provider],
|
||||
});
|
||||
```
|
||||
|
||||
## Adding a Custom Skill Type
|
||||
|
||||
The skills framework is designed for extensibility. While file-based and inline skills cover common
|
||||
scenarios, you can introduce entirely new skill types by subclassing the four base classes:
|
||||
|
||||
| Base class | Purpose |
|
||||
|-----------------------|-----------------------------------------------------|
|
||||
| `AgentSkillsSource` | Discovers and loads skills from a particular origin |
|
||||
| `AgentSkill` | Holds metadata, content, resources, and scripts |
|
||||
| `AgentSkillResource` | Provides supplementary content to a skill |
|
||||
| `AgentSkillScript` | Represents an executable action within a skill |
|
||||
|
||||
The example below implements a **cloud-based skill type** where skills, resources, and scripts are
|
||||
all stored in and executed through a remote cloud service (e.g., Azure Blob Storage + Azure Functions).
|
||||
|
||||
### Step 1 — Define a custom resource
|
||||
|
||||
A `CloudSkillResource` reads resource content from a cloud storage endpoint instead of the local
|
||||
filesystem:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A skill resource backed by a cloud storage endpoint.
|
||||
/// </summary>
|
||||
public sealed class CloudSkillResource : AgentSkillResource
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudSkillResource(string name, Uri blobUri, HttpClient httpClient, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
BlobUri = blobUri ?? throw new ArgumentNullException(nameof(blobUri));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI of the cloud blob that holds this resource's content.
|
||||
/// </summary>
|
||||
public Uri BlobUri { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> ReadAsync(
|
||||
IServiceProvider? serviceProvider = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _httpClient.GetStringAsync(BlobUri, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 — Define a custom script
|
||||
|
||||
A `CloudSkillScript` executes a script by calling a cloud function endpoint, passing arguments as
|
||||
the request body:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A skill script executed via a cloud function endpoint.
|
||||
/// </summary>
|
||||
public sealed class CloudSkillScript : AgentSkillScript
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudSkillScript(string name, Uri functionUri, HttpClient httpClient, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
FunctionUri = functionUri ?? throw new ArgumentNullException(nameof(functionUri));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI of the cloud function that runs this script.
|
||||
/// </summary>
|
||||
public Uri FunctionUri { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<object?> RunAsync(
|
||||
AgentSkill skill,
|
||||
AIFunctionArguments arguments,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(arguments);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(FunctionUri, content, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 — Define a custom skill
|
||||
|
||||
A `CloudSkill` bundles cloud-specific metadata (e.g., the base endpoint) with the standard skill
|
||||
shape:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// An <see cref="AgentSkill"/> whose content, resources, and scripts are stored in a cloud service.
|
||||
/// </summary>
|
||||
public sealed class CloudSkill : AgentSkill
|
||||
{
|
||||
public CloudSkill(
|
||||
AgentSkillFrontmatter frontmatter,
|
||||
string content,
|
||||
Uri endpoint,
|
||||
IReadOnlyList<AgentSkillResource>? resources = null,
|
||||
IReadOnlyList<AgentSkillScript>? scripts = null)
|
||||
{
|
||||
Frontmatter = frontmatter ?? throw new ArgumentNullException(nameof(frontmatter));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
|
||||
Resources = resources;
|
||||
Scripts = scripts;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base cloud endpoint for this skill.
|
||||
/// </summary>
|
||||
public Uri Endpoint { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4 — Define a custom source
|
||||
|
||||
A `CloudSkillsSource` discovers skills from a cloud catalog API and constructs `CloudSkill`
|
||||
instances with their associated resources and scripts:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// A skill source that discovers and loads skills from a cloud catalog API.
|
||||
/// </summary>
|
||||
public sealed class CloudSkillsSource : AgentSkillsSource
|
||||
{
|
||||
private readonly Uri _catalogUri;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CloudSkillsSource(Uri catalogUri, HttpClient httpClient)
|
||||
{
|
||||
_catalogUri = catalogUri ?? throw new ArgumentNullException(nameof(catalogUri));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Fetch the skill catalog from the cloud service.
|
||||
var json = await _httpClient.GetStringAsync(_catalogUri, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var catalog = JsonSerializer.Deserialize<CloudSkillCatalog>(json)!;
|
||||
|
||||
var skills = new List<AgentSkill>();
|
||||
|
||||
foreach (var entry in catalog.Skills)
|
||||
{
|
||||
var frontmatter = new AgentSkillFrontmatter(entry.Name, entry.Description);
|
||||
|
||||
// Build cloud-backed resources.
|
||||
var resources = entry.Resources
|
||||
.Select(r => new CloudSkillResource(r.Name, r.BlobUri, _httpClient, r.Description))
|
||||
.ToList<AgentSkillResource>();
|
||||
|
||||
// Build cloud-backed scripts.
|
||||
var scripts = entry.Scripts
|
||||
.Select(s => new CloudSkillScript(s.Name, s.FunctionUri, _httpClient, s.Description))
|
||||
.ToList<AgentSkillScript>();
|
||||
|
||||
skills.Add(new CloudSkill(frontmatter, entry.Content, entry.Endpoint, resources, scripts));
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5 — Register with the builder
|
||||
|
||||
Use `UseSource` to wire the custom source into the provider:
|
||||
|
||||
```csharp
|
||||
var httpClient = new HttpClient();
|
||||
|
||||
var provider = new AgentSkillsProviderBuilder()
|
||||
.UseSource(new CloudSkillsSource(
|
||||
new Uri("https://my-service.example.com/skills/catalog"),
|
||||
httpClient))
|
||||
// Mix with other source types if needed:
|
||||
.UseFileSkill("/local/skills", scriptRunner)
|
||||
.UseInlineSkills(someInlineSkill)
|
||||
.Build();
|
||||
```
|
||||
|
||||
The `AgentSkillsProvider` handles all skill types uniformly — any combination of file-based, inline,
|
||||
class-based, and custom skills can coexist in the same provider. Custom skills automatically
|
||||
participate in the model-facing tools (`load_skill`, `read_skill_resource`, `run_skill_script`),
|
||||
filtering, deduplication, and caching — no additional integration work is required.
|
||||
|
||||
## Script Representation: `AgentSkillScript` vs `AIFunction`
|
||||
|
||||
Two approaches were considered for representing executable scripts within skills:
|
||||
|
||||
### Option A — Custom `AgentSkillScript` abstract base class (original design)
|
||||
|
||||
Scripts are modeled as a custom `AgentSkillScript` abstract class with `Name`, `Description`, and
|
||||
`RunAsync(AgentSkill, AIFunctionArguments, CancellationToken)`. Concrete implementations:
|
||||
`AgentInlineSkillScript` (wraps a delegate/`AIFunction`) and `AgentFileSkillScript` (wraps a file path + executor delegate).
|
||||
|
||||
```csharp
|
||||
// Base type
|
||||
public abstract class AgentSkillScript
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
// AgentSkill exposes scripts as:
|
||||
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
||||
|
||||
// Inline script wraps an AIFunction internally
|
||||
var script = new AgentInlineSkillScript(ConvertUnits, "convert");
|
||||
|
||||
// Pre-built AIFunction must be wrapped
|
||||
var script = new AgentInlineSkillScript(myAIFunction);
|
||||
|
||||
// Class-based skill declares scripts as:
|
||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
|
||||
[
|
||||
new AgentInlineSkillScript(ConvertUnits, "convert"),
|
||||
];
|
||||
|
||||
// Provider executes scripts by passing the owning skill:
|
||||
await script.RunAsync(skill, arguments, cancellationToken);
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Explicit skill context at execution time.** `RunAsync` receives the owning `AgentSkill`, so any script can access skill metadata or resources during execution without requiring construction-time wiring.
|
||||
- **Self-contained abstraction.** A dedicated type communicates clearly that scripts are a skills-framework concept, separate from general-purpose AI functions.
|
||||
- **Easier extensibility for custom script types.** Third-party implementations can subclass `AgentSkillScript` and access the owning skill in `RunAsync` without special setup.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Wrapper overhead.** `AgentInlineSkillScript` is a thin pass-through around `AIFunction` — it adds a class, a constructor, and an indirection layer for no behavioral difference.
|
||||
- **Parallel abstraction.** `AgentSkillScript` and `AIFunction` serve overlapping purposes (named callable with arguments), creating two parallel hierarchies for the same concept.
|
||||
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillScript` to use them as scripts, adding ceremony.
|
||||
|
||||
### Option B — Reuse `AIFunction` directly
|
||||
|
||||
Scripts are represented as `AIFunction` (from `Microsoft.Extensions.AI`). `AgentSkill.Scripts` returns
|
||||
`IReadOnlyList<AIFunction>?`. `AgentInlineSkillScript` is eliminated entirely — callers use
|
||||
`AIFunctionFactory.Create(delegate, name: ...)` or pass `AIFunction` instances directly.
|
||||
`AgentFileSkillScript` becomes an `AIFunction` subclass that captures its owning `AgentFileSkill` via
|
||||
an internal back-reference set during construction.
|
||||
|
||||
```csharp
|
||||
// AgentSkill exposes scripts as AIFunction directly:
|
||||
public abstract IReadOnlyList<AIFunction>? Scripts { get; }
|
||||
|
||||
// Inline scripts use AIFunctionFactory — no wrapper class needed
|
||||
var skill = new AgentInlineSkill("my-skill", "desc", "instructions");
|
||||
skill.AddScript(ConvertUnits, "convert"); // delegate
|
||||
skill.AddScript(myAIFunction); // pre-built AIFunction — no wrapping
|
||||
|
||||
// Class-based skill declares scripts as:
|
||||
public override IReadOnlyList<AIFunction>? Scripts { get; } =
|
||||
[
|
||||
AIFunctionFactory.Create(ConvertUnits, name: "convert"),
|
||||
];
|
||||
|
||||
// Provider executes scripts via standard AIFunction invocation:
|
||||
await script.InvokeAsync(arguments, cancellationToken);
|
||||
|
||||
// File-based scripts extend AIFunction and capture the owning skill internally:
|
||||
public sealed class AgentFileSkillScript : AIFunction
|
||||
{
|
||||
internal AgentFileSkill? Skill { get; set; } // set by AgentFileSkill constructor
|
||||
|
||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _executor(Skill!, this, arguments, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Fewer types.** Eliminates `AgentSkillScript` and `AgentInlineSkillScript`, reducing the public API surface by two classes.
|
||||
- **Seamless interop.** Any `AIFunction` — whether from `AIFunctionFactory`, a custom subclass, or an external library — can be used as a skill script with zero wrapping.
|
||||
- **Consistent with `Microsoft.Extensions.AI` ecosystem.** Scripts share the same type as tool functions used by `IChatClient` and `FunctionInvokingChatClient`, reducing conceptual overhead for developers already familiar with the ecosystem.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **No owning-skill context in invocation signature.** `AIFunction.InvokeAsync` does not accept an `AgentSkill` parameter, so `AgentFileSkillScript` must capture its owning skill via an internal setter during construction. This adds a construction-order dependency: the skill must set the back-reference on its scripts.
|
||||
- **Custom script types lose automatic skill access.** Third-party `AIFunction` subclasses that need the owning skill must implement their own mechanism (e.g., constructor injection, closure capture) instead of receiving it as a method parameter.
|
||||
- **Semantic overloading.** `AIFunction` now means both "a tool the model can call" and "a script within a skill", which could blur the distinction for framework users.
|
||||
|
||||
## Resource Representation: `AgentSkillResource` vs `AIFunction`
|
||||
|
||||
Two approaches were considered for representing skill resources (supplementary content such as references, assets, or dynamic data):
|
||||
|
||||
### Option A — Custom `AgentSkillResource` abstract base class (original design)
|
||||
|
||||
Resources are modeled as a custom `AgentSkillResource` abstract class with `Name`, `Description`, and
|
||||
`ReadAsync(IServiceProvider?, CancellationToken)`. Concrete implementations:
|
||||
`AgentInlineSkillResource` (static value, delegate, or `AIFunction` wrapper) and `AgentFileSkillResource` (reads file content from disk).
|
||||
|
||||
```csharp
|
||||
// Base type
|
||||
public abstract class AgentSkillResource
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Description { get; }
|
||||
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
// AgentSkill exposes resources as:
|
||||
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
|
||||
|
||||
// Static resource
|
||||
var resource = new AgentInlineSkillResource("static content", "my-resource");
|
||||
|
||||
// Dynamic resource (delegate)
|
||||
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
|
||||
|
||||
// Pre-built AIFunction must be wrapped
|
||||
var resource = new AgentInlineSkillResource(myAIFunction);
|
||||
|
||||
// Class-based skill declares resources as:
|
||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
|
||||
[
|
||||
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
|
||||
];
|
||||
|
||||
// Provider reads resources via:
|
||||
await resource.ReadAsync(serviceProvider, cancellationToken);
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Clear semantic distinction.** A dedicated `AgentSkillResource` type distinguishes resources (data providers) from scripts (executable actions), making the API self-documenting.
|
||||
- **Purpose-built API.** `ReadAsync` communicates intent better than `InvokeAsync` for a data-access operation.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Wrapper overhead.** `AgentInlineSkillResource` wraps `AIFunction` internally for delegate/function cases — adding a class and indirection for no behavioral difference.
|
||||
- **Parallel abstraction.** `AgentSkillResource` and `AIFunction` serve overlapping purposes (named callable that returns data), creating two parallel hierarchies.
|
||||
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillResource`, adding ceremony.
|
||||
|
||||
### Option B — Reuse `AIFunction` directly
|
||||
|
||||
Resources are represented as `AIFunction`. `AgentSkill.Resources` returns `IReadOnlyList<AIFunction>?`.
|
||||
`AgentInlineSkillResource` becomes an `AIFunction` subclass (retained as a convenience for the static-value
|
||||
pattern: `new AgentInlineSkillResource("data", "name")`). `AgentFileSkillResource` becomes an `AIFunction`
|
||||
subclass that reads file content.
|
||||
|
||||
```csharp
|
||||
// AgentSkill exposes resources as AIFunction directly:
|
||||
public abstract IReadOnlyList<AIFunction>? Resources { get; }
|
||||
|
||||
// Static resource — AgentInlineSkillResource is retained as a convenience AIFunction subclass
|
||||
var resource = new AgentInlineSkillResource("static content", "my-resource");
|
||||
|
||||
// Dynamic resource — AgentInlineSkillResource wraps delegate as AIFunction
|
||||
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
|
||||
|
||||
// Pre-built AIFunction can be used directly — no wrapping needed
|
||||
skill.AddResource(myAIFunction);
|
||||
|
||||
// Class-based skill declares resources as:
|
||||
public override IReadOnlyList<AIFunction>? Resources { get; } =
|
||||
[
|
||||
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
|
||||
];
|
||||
|
||||
// Provider reads resources via standard AIFunction invocation:
|
||||
await resource.InvokeAsync(arguments, cancellationToken);
|
||||
|
||||
// File-based resources extend AIFunction directly:
|
||||
internal sealed class AgentFileSkillResource : AIFunction
|
||||
{
|
||||
public string FullPath { get; }
|
||||
|
||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
return await File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- **Fewer base types.** Eliminates the `AgentSkillResource` abstract class, reducing the public API surface.
|
||||
- **Seamless interop.** Any `AIFunction` can be used as a skill resource with zero wrapping.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Loss of semantic distinction.** Resources and scripts are now both `AIFunction`, which could make it less obvious which list a function belongs to when reading code.
|
||||
- **Static values require a wrapper.** Unlike the original `ReadAsync` which could return a stored value directly, `AIFunction.InvokeAsync` implies invocation. `AgentInlineSkillResource` is retained as a convenience subclass to handle the static-value case, so this is not eliminated — just moved to a different class.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### 1. Keep `AgentSkillResource` and `AgentSkillScript` (Option A for both sections)
|
||||
|
||||
We are staying with the custom `AgentSkillResource` and `AgentSkillScript` model classes instead of reusing `AIFunction`:
|
||||
|
||||
- **Resources have no parameters.** If a consumer provides an `AIFunction` with parameters, those parameters will never be advertised to the LLM, and the resulting call will fail.
|
||||
- **Approval breaks for `AIFunction`-based representations.** When a resource or script represented by an `AIFunction` is configured with approval, the second approval invocation will not work correctly.
|
||||
- **Injecting the owning skill into an `AIFunction`-based script is problematic.** Constructor injection would introduce a circular reference between the skill and the script. An internal property setter is possible but adds coupling.
|
||||
|
||||
### 2. Make all agent skill classes internal
|
||||
|
||||
All agent-skill-related classes are made `internal` to minimize the public API surface while the feature matures. We can reconsider and promote types to `public` later based on community signal.
|
||||
|
||||
This leaves two public entry points:
|
||||
|
||||
- **`AgentSkillsProvider`** — use directly when all skills come from a single source and filtering is not needed.
|
||||
- **`AgentSkillsProviderBuilder`** — use when mixing skill types or when filtering support is required.
|
||||
|
||||
### 3. Caching at provider level
|
||||
|
||||
Caching of tools and instructions is implemented inside `AgentSkillsProvider` rather than as an external decorator. Recreating tools and instructions on every provider call is wasteful, and a caching decorator sitting outside the provider would not have the information needed to cache them effectively.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-03-20
|
||||
deciders: eavanvalkenburg, sphenry, chetantoshnival
|
||||
consulted: taochenosu, moonbox3, dmytrostruk, giles17, alliscode
|
||||
---
|
||||
|
||||
# Provider-Leading Client Design & OpenAI Package Extraction
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The `agent-framework-core` package currently bundles OpenAI and Azure OpenAI client implementations along with their dependencies (`openai`, `azure-identity`, `azure-ai-projects`, `packaging`). This makes core heavier than necessary for users who don't use OpenAI, and it conflates the core abstractions with a specific provider implementation. Additionally, the current class naming (`OpenAIResponsesClient`, `OpenAIChatClient`) is based on the underlying OpenAI API names rather than what users actually want to do, making discoverability harder for newcomers.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Lightweight core**: Core should only contain abstractions, middleware infrastructure, and telemetry — no provider-specific code or dependencies.
|
||||
- **Discoverability-first**: Import namespaces should guide users to the right client. `from agent_framework.openai import ...` should surface all OpenAI-related clients; `from agent_framework.azure import ...` should surface Foundry, Azure AI, and other Azure-specific classes.
|
||||
- **Provider-leading naming**: The primary client name should reflect the provider, not the underlying API. The Responses API is now the recommended default for OpenAI, so its client should be called `OpenAIChatClient` (not `OpenAIResponsesClient`).
|
||||
- **Clean separation of concerns**: Azure-specific deprecated wrappers belong in the azure-ai package, not in the OpenAI package.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Keep OpenAI in core**: Simpler but keeps core heavy; doesn't help discoverability.
|
||||
- **Extract OpenAI with Azure wrappers in the OpenAI package**: Keeps Azure OpenAI wrappers alongside OpenAI code, but pollutes the OpenAI package with Azure concerns.
|
||||
- **Extract OpenAI, place Azure wrappers in azure-ai**: Clean separation; the OpenAI package has zero Azure dependencies; deprecated Azure wrappers live in a single file in azure-ai for easy future deletion.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: "Extract OpenAI, place Azure wrappers in azure-ai", because it achieves the lightest core, cleanest OpenAI package, and the most maintainable deprecation path.
|
||||
|
||||
Key changes:
|
||||
|
||||
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
|
||||
2. **Class renames**: `OpenAIResponsesClient` → `OpenAIChatClient` (Responses API), `OpenAIChatClient` → `OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
|
||||
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
|
||||
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
|
||||
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
|
||||
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
|
||||
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_RESPONSES_MODEL_ID` / `OPENAI_CHAT_MODEL_ID`).
|
||||
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
|
||||
|
||||
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
|
||||
|
||||
The existing `AzureAIClient` combines two concerns: CRUD lifecycle management (creating/deleting agents on the service) and runtime communication (sending messages via the Responses API). The new design removes CRUD entirely — users connect to agents that already exist in Foundry.
|
||||
|
||||
**Two approaches were considered:**
|
||||
|
||||
**Option A — `FoundryAgentClient` only (public ChatClient):**
|
||||
Users compose `Agent(client=FoundryAgentClient(...), tools=[...])`. This follows the universal `Agent(client=X)` pattern used by every other provider. However, a "client" that wraps a named remote agent (with `agent_name` as a constructor param) is semantically odd — clients typically wrap a model endpoint, not a specific agent.
|
||||
|
||||
**Option B — `FoundryAgent` (Agent subclass) + private `_FoundryAgentChatClient` and public `RawFoundryAgentChatClient`:**
|
||||
Users write `FoundryAgent(agent_name="my-agent", ...)` for the common case. Internally, `FoundryAgent` creates a `_FoundryAgentChatClient` and passes it to the standard `Agent` base class. For advanced customization, users pass `client_type=RawFoundryAgentChatClient` (or a custom subclass) to control the client middleware layers. The `Agent(client=RawFoundryAgentChatClient(...))` composition pattern still works for users who prefer it.
|
||||
|
||||
**Chosen option: Option B**, because:
|
||||
- The common case (`FoundryAgent(...)`) is a single object with no boilerplate.
|
||||
- `client_type=` gives full control over client middleware without parameter duplication — the agent forwards connection params to the client internally.
|
||||
- `RawFoundryAgent(RawAgent)` and `FoundryAgent(Agent)` mirror the established `RawAgent`/`Agent` pattern.
|
||||
- Runtime validation (only `FunctionTool` allowed) lives in `RawFoundryAgentChatClient._prepare_options`, ensuring it applies regardless of how the client is used — through `FoundryAgent`, `Agent(client=...)`, or any custom composition.
|
||||
|
||||
**Public classes:**
|
||||
- `RawFoundryAgentChatClient(RawOpenAIChatClient)` — Responses API client that injects agent reference and validates tools. Extension point for custom client middleware.
|
||||
- `RawFoundryAgent(RawAgent)` — Agent without agent-level middleware/telemetry.
|
||||
- `FoundryAgent(AgentTelemetryLayer, AgentMiddlewareLayer, RawFoundryAgent)` — Recommended production agent.
|
||||
|
||||
**Internal (private):**
|
||||
- `_FoundryAgentChatClient` — Full client with function invocation, chat middleware, and telemetry layers. Created automatically by `FoundryAgent`; users customize via `client_type=RawFoundryAgentChatClient` or a custom subclass.
|
||||
|
||||
**Deprecated:**
|
||||
- `AzureAIClient` — replaced by `FoundryAgent` (which uses `FoundryAgentClient` internally).
|
||||
- `AzureAIAgentClient` — refers to V1 Agents Service API, no direct replacement.
|
||||
- `AzureAIProjectAgentProvider` — replaced by `FoundryAgent`.
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2026-03-23
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
|
||||
consulted:
|
||||
informed:
|
||||
---
|
||||
|
||||
# Chat History Persistence Consistency
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
When using `ChatClientAgent` with tools, the `FunctionInvokingChatClient` (FIC) loops multiple times — service call → tool execution → service call → … — before producing a final response. There are two points of discrepancy between how chat history is stored by the framework's `ChatHistoryProvider` and how the underlying AI service stores chat history (e.g., OpenAI Responses with `store=true`):
|
||||
|
||||
1. **Persistence timing**: The AI service persists messages after *each* service call within the FIC loop. The `ChatHistoryProvider` currently persists messages only once, at the *end* of the full agent run (after all FIC loop iterations complete).
|
||||
|
||||
2. **Trailing `FunctionResultContent` storage**: When tool calling is terminated mid-loop (e.g., via `FunctionInvokingChatClient` termination filters), the final response from the agent may contain `FunctionResultContent` that was never sent to a subsequent service call. The AI service never stores this trailing `FunctionResultContent`, but the `ChatHistoryProvider` currently stores all response content, including the trailing `FunctionResultContent`.
|
||||
|
||||
These discrepancies mean that a `ChatHistoryProvider`-managed conversation and a service-managed conversation can diverge in content and structure, even when processing the same interactions.
|
||||
|
||||
### Practical Impact: Resuming After Tool-Call Termination
|
||||
|
||||
Today, users of `AIAgent` get different behaviors depending on whether chat history is stored service-side or in a `ChatHistoryProvider`. This creates concrete challenges — for example, when the function call loop is terminated and the user wants to resume the conversation in a subsequent run. With service-stored history, the trailing `FunctionResultContent` is never persisted, so the last stored message is the `FunctionCallContent` from the service. With `ChatHistoryProvider`-stored history, the trailing `FunctionResultContent` *is* persisted. The user cannot know whether the last `FunctionResultContent` is in the chat history or not without inspecting the storage mechanism, making it difficult to write resumption logic that works correctly regardless of the storage backend.
|
||||
|
||||
### Relationship Between the Two Discrepancies
|
||||
|
||||
The persistence timing and `FunctionResultContent` trimming behaviors are interrelated:
|
||||
|
||||
- **Per-service-call persistence**: When messages are persisted after each individual service call, trailing `FunctionResultContent` trimming is unnecessary. If tool calling is terminated, the `FunctionResultContent` from the terminated call was never sent to a subsequent service call, so it is never persisted. The per-service-call approach naturally matches the service's behavior.
|
||||
|
||||
- **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history.
|
||||
- **B. Atomicity**: A run that fails mid-way through a multi-step tool-calling loop should not leave chat history in a partially-updated state, unless the user explicitly opts into that behavior.
|
||||
- **C. Recoverability**: For long-running tool-calling loops, it should be possible to recover intermediate progress if the process is interrupted, rather than losing all work from the current run.
|
||||
- **D. Simplicity**: The default behavior should be easy to understand and predict for most users, without requiring knowledge of the FIC loop internals.
|
||||
- **E. Flexibility**: Regardless of the chosen default, users should be able to opt into the alternative behavior.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming
|
||||
- Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Option 1: Per-run persistence with opt-in FRC trimming
|
||||
|
||||
Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as an opt-in behavior to improve consistency with service storage.
|
||||
|
||||
- Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B.
|
||||
- Good, because the mental model is simple: one run = one history update, satisfying driver D.
|
||||
- Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A.
|
||||
- Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A.
|
||||
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C.
|
||||
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
|
||||
|
||||
### Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
||||
|
||||
Introduce an optional RequirePerServiceCallChatHistoryPersistence setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
|
||||
|
||||
Settings:
|
||||
- `RequirePerServiceCallChatHistoryPersistence` = `true`
|
||||
|
||||
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A.
|
||||
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
|
||||
- Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity.
|
||||
- Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`.
|
||||
- Bad, because the mental model is more complex: a single run may produce multiple history updates, partially failing driver D.
|
||||
- Neutral, because users can opt out to per-run persistence if they prefer atomicity, satisfying driver E.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `RequirePerServiceCallChatHistoryPersistence` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
|
||||
|
||||
### Configuration Matrix
|
||||
|
||||
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `RequirePerServiceCallChatHistoryPersistence`:
|
||||
|
||||
| `UseProvidedChatClientAsIs` | `RequirePerServiceCallChatHistoryPersistence` | Behavior |
|
||||
|---|---|---|
|
||||
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. |
|
||||
| `false` | `true` | **Per-service-call persistence (simulated).** A `PerServiceCallChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
|
||||
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. |
|
||||
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `PerServiceCallChatHistoryPersistingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B.
|
||||
- Good, because the default mental model is simple: one run = one history update, satisfying driver D.
|
||||
- Good, because users who opt into `RequirePerServiceCallChatHistoryPersistence` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
|
||||
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in.
|
||||
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled.
|
||||
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`.
|
||||
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
|
||||
- Neutral, because users who want per-service-call consistency can opt in via `RequirePerServiceCallChatHistoryPersistence = true`, satisfying driver E.
|
||||
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
#### Conversation ID Consistency
|
||||
|
||||
When `RequirePerServiceCallChatHistoryPersistence` is enabled, the `PerServiceCallChatHistoryPersistingChatClient`
|
||||
decorator also updates `session.ConversationId` after each service call. This handles two scenarios:
|
||||
|
||||
1. **Framework-managed chat history** — the decorator sets a sentinel `ConversationId` on the response
|
||||
so that `FunctionInvokingChatClient` treats the conversation as service-managed (clearing accumulated
|
||||
history between iterations and not injecting duplicate `FunctionCallContent` during approval processing).
|
||||
|
||||
2. **Service-stored chat history** — when the service returns a real `ConversationId`, the decorator
|
||||
updates `session.ConversationId` immediately after each service call, rather than deferring the update
|
||||
to the end of the run. This ensures intermediate ConversationId changes are captured even if the
|
||||
process is interrupted mid-loop.
|
||||
|
||||
For some service-stored scenarios (e.g., the Conversations API with the Responses API), there is only
|
||||
one thread with one ID, so every service call returns the same ConversationId and this per-call update
|
||||
makes no practical difference. Enabling `RequirePerServiceCallChatHistoryPersistence` ensures consistent
|
||||
per-service-call behavior across all service types regardless of how they manage ConversationIds.
|
||||
|
||||
+1
-1
@@ -462,7 +462,7 @@ class FoundryEvals:
|
||||
### Azure AI: FoundryEvals Constants
|
||||
|
||||
```python
|
||||
from agent_framework_azure_ai import FoundryEvals
|
||||
from agent_framework.foundry import FoundryEvals
|
||||
|
||||
evaluators = [FoundryEvals.RELEVANCE, FoundryEvals.TOOL_CALL_ACCURACY]
|
||||
```
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: verify-samples-tool
|
||||
description: How to use the verify-samples tool to run, verify, and manage sample definitions in the Agent Framework repository. Use this when adding, updating, or running sample verification.
|
||||
---
|
||||
|
||||
# verify-samples Tool
|
||||
|
||||
The `verify-samples` project (`dotnet/eng/verify-samples/`) is an automated tool that runs sample projects and verifies their output using deterministic checks and AI-powered verification.
|
||||
|
||||
## Running verify-samples
|
||||
|
||||
```bash
|
||||
cd dotnet
|
||||
|
||||
# Run all samples across all categories
|
||||
dotnet run --project eng/verify-samples -- --log results.log --csv results.csv
|
||||
|
||||
# Run a specific category
|
||||
dotnet run --project eng/verify-samples -- --category 02-agents --log results.log
|
||||
|
||||
# Run specific samples by name
|
||||
dotnet run --project eng/verify-samples -- Agent_Step02_StructuredOutput Agent_Step09_AsFunctionTool
|
||||
|
||||
# Control parallelism (default 8)
|
||||
dotnet run --project eng/verify-samples -- --parallel 8 --log results.log
|
||||
|
||||
# Combine options
|
||||
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv
|
||||
```
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
The tool itself needs:
|
||||
- `AZURE_OPENAI_ENDPOINT` — for the AI verification agent
|
||||
- `AZURE_OPENAI_DEPLOYMENT_NAME` (optional, defaults to `gpt-5-mini`)
|
||||
|
||||
Individual samples require their own env vars (e.g., `AZURE_AI_PROJECT_ENDPOINT`). The tool automatically checks and skips samples with missing env vars.
|
||||
|
||||
### Output Files
|
||||
|
||||
- `--log results.log` — detailed per-sample log with stdout/stderr, AI reasoning, and a summary
|
||||
- `--csv results.csv` — tabular summary with Sample, ProjectPath, Status, FailedChecks, and Failures columns
|
||||
|
||||
## Sample Categories
|
||||
|
||||
Definitions are in the `dotnet/eng/verify-samples/` directory:
|
||||
|
||||
| Category | Config File | Registered Key |
|
||||
|----------|-------------|----------------|
|
||||
| 01-get-started | `GetStartedSamples.cs` | `01-get-started` |
|
||||
| 02-agents | `AgentsSamples.cs` | `02-agents` |
|
||||
| 03-workflows | `WorkflowSamples.cs` | `03-workflows` |
|
||||
|
||||
Categories are registered in `VerifyOptions.cs` in the `s_sampleSets` dictionary.
|
||||
|
||||
## SampleDefinition Properties
|
||||
|
||||
Each sample is defined as a `SampleDefinition` in the appropriate config file. Key properties:
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
// Required: Display name for the sample
|
||||
Name = "Agent_Step02_StructuredOutput",
|
||||
|
||||
// Required: Relative path from dotnet/ to the sample project directory
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step02_StructuredOutput",
|
||||
|
||||
// Environment variables the sample requires (throws if missing)
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
|
||||
// Environment variables with defaults that would prompt on console if unset
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
|
||||
// Skip this sample with a reason (for structural issues only)
|
||||
SkipReason = null, // or "Requires external service X."
|
||||
|
||||
// Deterministic checks: substrings that must appear in stdout
|
||||
MustContain = ["=== Section Header ==="],
|
||||
|
||||
// Substrings that must NOT appear in stdout
|
||||
MustNotContain = [],
|
||||
|
||||
// If true, only MustContain checks are used (no AI verification)
|
||||
IsDeterministic = false,
|
||||
|
||||
// AI verification: natural-language descriptions of expected output
|
||||
// Each entry describes one aspect to verify independently
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show structured person information with Name, Age, and Occupation fields.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
|
||||
// Stdin inputs to feed to the sample (for interactive samples)
|
||||
Inputs = ["Y", "Y", "Y"],
|
||||
|
||||
// Delay between stdin inputs in ms (default 2000, increase for LLM calls between inputs)
|
||||
InputDelayMs = 3000,
|
||||
}
|
||||
```
|
||||
|
||||
## How to Add a New Sample Definition
|
||||
|
||||
1. **Check the sample's Program.cs** to understand:
|
||||
- What environment variables it reads (look for `GetEnvironmentVariable`)
|
||||
- Whether it needs stdin input (look for `Console.ReadLine`, `Application.GetInput`)
|
||||
- Whether it has an external loop (look for `EXIT` patterns in YAML workflows)
|
||||
- What output it produces (section headers, markers, expected behavior)
|
||||
- Whether it exits on its own or runs as a server
|
||||
|
||||
2. **Choose the right verification strategy:**
|
||||
- **Deterministic** (`IsDeterministic = true`): Use `MustContain` for samples with fixed output strings. No AI verification.
|
||||
- **AI-verified** (default): Use `ExpectedOutputDescription` with semantic descriptions. Write expectations that are flexible enough for non-deterministic LLM output.
|
||||
- **Both**: Use `MustContain` for fixed markers AND `ExpectedOutputDescription` for LLM-generated content.
|
||||
|
||||
3. **Set `SkipReason` only for structural issues:**
|
||||
- Web servers that don't exit
|
||||
- Multi-process client/server architectures
|
||||
- Samples requiring external infrastructure (MCP servers you can't reach, Docker, etc.)
|
||||
- Do NOT skip for missing env vars — the tool checks those dynamically.
|
||||
|
||||
4. **For interactive samples, provide `Inputs`:**
|
||||
- Samples using `Application.GetInput(args)` need one initial input
|
||||
- Samples with `Console.ReadLine()` approval loops need `"Y"` inputs
|
||||
- YAML workflows with `externalLoop` need `"EXIT"` as the last input
|
||||
- Set `InputDelayMs` to 3000-8000ms for samples with LLM calls between inputs
|
||||
|
||||
5. **Add the definition** to the appropriate config file (e.g., `AgentsSamples.cs`) in the `All` list.
|
||||
|
||||
6. **Register new categories** (if needed) in `VerifyOptions.cs` `s_sampleSets` dictionary.
|
||||
|
||||
### Writing Good ExpectedOutputDescription
|
||||
|
||||
- Write descriptions that are **semantically flexible** — LLM output varies between runs
|
||||
- Each array entry should describe **one independent aspect** to verify
|
||||
- Always include `"The output should not contain error messages or stack traces."` as the last entry
|
||||
- Avoid exact wording expectations — use "should mention", "should contain information about", "should show"
|
||||
- Bad: `"The output should say 'The weather in Amsterdam is cloudy with a high of 15°C'"`
|
||||
- Good: `"The output should contain weather information about Amsterdam mentioning cloudy weather with a high of 15°C."`
|
||||
|
||||
### Example: Simple LLM Sample
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_With_AzureOpenAIChatCompletion",
|
||||
ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a joke about a pirate.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Deterministic Sample
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_GenerateCode",
|
||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
||||
IsDeterministic = true,
|
||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Interactive Sample with Approval Loop
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "FoundryAgent_Hosted_MCP",
|
||||
ProjectPath = "samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["Y", "Y", "Y", "Y", "Y"],
|
||||
InputDelayMs = 5000,
|
||||
ExpectedOutputDescription = ["The output should show an agent using the Microsoft Learn MCP tool with approval prompts."],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Declarative Workflow with External Loop
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_FunctionTools",
|
||||
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What are today's specials?", "EXIT"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow calling function tools to answer a question about restaurant specials."],
|
||||
},
|
||||
```
|
||||
|
||||
### Example: Skipped Sample
|
||||
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_MCP_Server",
|
||||
ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_Server",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
|
||||
},
|
||||
```
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<IsReleaseCandidate>false</IsReleaseCandidate>
|
||||
<IsGenerallyAvailable>false</IsGenerallyAvailable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Solution>
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<BuildType Name="Debug" />
|
||||
<BuildType Name="Publish" />
|
||||
@@ -7,6 +7,7 @@
|
||||
<Folder Name="/Samples/">
|
||||
<File Path="samples/AGENTS.md" />
|
||||
<File Path="samples/README.md" />
|
||||
<Project Path="eng/verify-samples/verify-samples.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/01-get-started/">
|
||||
<Project Path="samples/01-get-started/01_hello_agent/01_hello_agent.csproj" />
|
||||
@@ -57,6 +58,7 @@
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Agent_Step17_AdditionalAIContext.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
|
||||
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||
@@ -76,6 +78,8 @@
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
|
||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
@@ -101,7 +105,8 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentSkills/">
|
||||
<File Path="samples/02-agents/AgentSkills/README.md" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||
@@ -118,6 +123,34 @@
|
||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentsWithFoundry/">
|
||||
<File Path="samples/02-agents/AgentsWithFoundry/README.md" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
|
||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentWithMemory/">
|
||||
<File Path="samples/02-agents/AgentWithMemory/README.md" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||
@@ -139,35 +172,7 @@
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/FoundryAgents/">
|
||||
<File Path="samples/02-agents/FoundryAgents/README.md" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Evaluations_Step01_RedTeaming/FoundryAgents_Evaluations_Step01_RedTeaming.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Evaluations_Step02_SelfReflection/FoundryAgents_Evaluations_Step02_SelfReflection.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step01.1_Basics/FoundryAgents_Step01.1_Basics.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step01.2_Running/FoundryAgents_Step01.2_Running.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/FoundryAgents_Step02_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/FoundryAgents_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step05_StructuredOutput/FoundryAgents_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step06_PersistedConversations/FoundryAgents_Step06_PersistedConversations.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step07_Observability/FoundryAgents_Step07_Observability.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step08_DependencyInjection/FoundryAgents_Step08_DependencyInjection.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step09_UsingMcpClientAsTools/FoundryAgents_Step09_UsingMcpClientAsTools.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/FoundryAgents_Step11_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step12_Middleware/FoundryAgents_Step12_Middleware.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step13_Plugins/FoundryAgents_Step13_Plugins.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/FoundryAgents_Step14_CodeInterpreter.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step15_ComputerUse/FoundryAgents_Step15_ComputerUse.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step16_FileSearch/FoundryAgents_Step16_FileSearch.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step17_OpenAPITools/FoundryAgents_Step17_OpenAPITools.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step18_BingCustomSearch/FoundryAgents_Step18_BingCustomSearch.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step19_SharePoint/FoundryAgents_Step19_SharePoint.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step20_MicrosoftFabric/FoundryAgents_Step20_MicrosoftFabric.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step21_WebSearch/FoundryAgents_Step21_WebSearch.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step22_MemorySearch/FoundryAgents_Step22_MemorySearch.csproj" />
|
||||
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step23_LocalMCP/FoundryAgents_Step23_LocalMCP.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
||||
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
||||
@@ -315,8 +320,8 @@
|
||||
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
|
||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml" />
|
||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
|
||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe console output with sample-name prefixes and colored status.
|
||||
/// </summary>
|
||||
internal sealed class ConsoleReporter
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Writes a complete prefixed line atomically to the console.
|
||||
/// </summary>
|
||||
public void WriteLineWithPrefix(string sampleName, string message, ConsoleColor? color = null)
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"[{sampleName}] ");
|
||||
if (color.HasValue)
|
||||
{
|
||||
Console.ForegroundColor = color.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the final summary table and elapsed time to the console.
|
||||
/// </summary>
|
||||
public void PrintSummary(
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
TimeSpan elapsed)
|
||||
{
|
||||
var passCount = orderedResults.Count(r => r.Passed);
|
||||
var failCount = orderedResults.Count(r => !r.Passed);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(new string('─', 60));
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine("SUMMARY");
|
||||
Console.ResetColor();
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
Console.ForegroundColor = result.Passed ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
Console.Write(result.Passed ? " ✓ " : " ✗ ");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine($"{result.SampleName}: {result.Summary}");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.Write(" ○ ");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine($"{name}: Skipped — {reason}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.Write("Results: ");
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.Write($"{passCount} passed");
|
||||
Console.ResetColor();
|
||||
|
||||
if (failCount > 0)
|
||||
{
|
||||
Console.Write(", ");
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Write($"{failCount} failed");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
if (skipped.Count > 0)
|
||||
{
|
||||
Console.Write(", ");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.Write($"{skipped.Count} skipped");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a CSV summary of sample verification results.
|
||||
/// </summary>
|
||||
internal static class CsvResultWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the results to a CSV file at the specified path.
|
||||
/// </summary>
|
||||
public static async Task WriteAsync(
|
||||
string path,
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
IReadOnlyList<SampleDefinition> samples)
|
||||
{
|
||||
var pathLookup = samples.ToDictionary(s => s.Name, s => s.ProjectPath);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Sample,ProjectPath,Status,FailedChecks,Failures");
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
var status = result.Passed ? "PASSED" : "FAILED";
|
||||
var failedChecks = result.Failures.Count;
|
||||
var failures = string.Join("; ", result.Failures);
|
||||
pathLookup.TryGetValue(result.SampleName, out var projectPath);
|
||||
sb.AppendLine($"{CsvEscape(result.SampleName)},{CsvEscape(projectPath ?? "")},{status},{failedChecks},{CsvEscape(failures)}");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
pathLookup.TryGetValue(name, out var projectPath);
|
||||
sb.AppendLine($"{CsvEscape(name)},{CsvEscape(projectPath ?? "")},SKIPPED,0,{CsvEscape(reason)}");
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(path, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a value for CSV: wraps in quotes if it contains commas, quotes, or newlines.
|
||||
/// </summary>
|
||||
private static string CsvEscape(string value)
|
||||
{
|
||||
if (value.Contains('"') || value.Contains(',') || value.Contains('\n') || value.Contains('\r'))
|
||||
{
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the expected behavior for each sample in 01-get-started.
|
||||
/// </summary>
|
||||
internal static class GetStartedSamples
|
||||
{
|
||||
public static IReadOnlyList<SampleDefinition> All { get; } =
|
||||
[
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "05_first_workflow",
|
||||
ProjectPath = "samples/01-get-started/05_first_workflow",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"UppercaseExecutor: HELLO, WORLD!",
|
||||
"ReverseTextExecutor: !DLROW ,OLLEH",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "01_hello_agent",
|
||||
ProjectPath = "samples/01-get-started/01_hello_agent",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a joke about a pirate.",
|
||||
"There should be two separate joke responses — one from a non-streaming call and one from a streaming call.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "02_add_tools",
|
||||
ProjectPath = "samples/01-get-started/02_add_tools",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain = [],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain information about the weather in Amsterdam.",
|
||||
"The response should mention that it is cloudy with a high of 15°C (or equivalent), since this comes from a tool that returns a canned response.",
|
||||
"There should be two responses — one from a non-streaming call and one from a streaming call.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "03_multi_turn",
|
||||
ProjectPath = "samples/01-get-started/03_multi_turn",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a joke about a pirate.",
|
||||
"After the initial joke, there should be a modified version that includes emojis and is told in the voice of a pirate's parrot.",
|
||||
"The pattern repeats: first a non-streaming pirate joke + parrot version, then a streaming pirate joke + parrot version.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "04_memory",
|
||||
ProjectPath = "samples/01-get-started/04_memory",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
">> Use session with blank memory",
|
||||
">> Use deserialized session with previously created memories",
|
||||
">> Read memories using memory component",
|
||||
"MEMORY - User Name:",
|
||||
"MEMORY - User Age:",
|
||||
">> Use new session with previously created memories",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"In the 'Use session with blank memory' section, the agent should respond to the user's messages. It may ask for the user's name or age if not yet known.",
|
||||
"In the 'Use deserialized session with previously created memories' section, the agent should correctly recall that the user's name is Ruaidhrí and age is 20.",
|
||||
"The 'MEMORY - User Name:' line should show 'Ruaidhrí' (or a close transliteration).",
|
||||
"The 'MEMORY - User Age:' line should show '20'.",
|
||||
"In the 'Use new session with previously created memories' section, the agent should know the user's name and age from the transferred memory.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "06_host_your_agent",
|
||||
ProjectPath = "samples/01-get-started/06_host_your_agent",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Incrementally writes a sequential (non-interleaved) log file, appending after each sample completes.
|
||||
/// Thread-safe: multiple parallel tasks may call write methods concurrently.
|
||||
/// </summary>
|
||||
internal sealed class LogFileWriter : IDisposable
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
public LogFileWriter(string path)
|
||||
{
|
||||
this._path = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._writeLock.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the log file header. Call once at the start of the run.
|
||||
/// </summary>
|
||||
public async Task WriteHeaderAsync()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Sample Verification Log — {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
||||
sb.AppendLine(new string('═', 72));
|
||||
sb.AppendLine();
|
||||
|
||||
await File.WriteAllTextAsync(this._path, sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a skipped-sample entry to the log file.
|
||||
/// </summary>
|
||||
public async Task WriteSkippedAsync(string name, string reason)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"── {name} ──");
|
||||
sb.AppendLine($"Status: SKIPPED — {reason}");
|
||||
sb.AppendLine();
|
||||
|
||||
await this.AppendAsync(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a completed sample's full output section to the log file.
|
||||
/// </summary>
|
||||
public async Task WriteSampleResultAsync(VerificationResult result)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(new string('─', 72));
|
||||
sb.AppendLine($"── {result.SampleName} ──");
|
||||
sb.AppendLine($"Status: {(result.Passed ? "PASSED" : "FAILED")}");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var line in result.LogLines)
|
||||
{
|
||||
sb.AppendLine(line);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.Stdout))
|
||||
{
|
||||
sb.AppendLine("--- stdout ---");
|
||||
sb.AppendLine(result.Stdout.TrimEnd());
|
||||
sb.AppendLine("--- end stdout ---");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.Stderr))
|
||||
{
|
||||
sb.AppendLine("--- stderr ---");
|
||||
sb.AppendLine(result.Stderr.TrimEnd());
|
||||
sb.AppendLine("--- end stderr ---");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (result.Failures.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Failures:");
|
||||
foreach (var failure in result.Failures)
|
||||
{
|
||||
sb.AppendLine($" ✗ {failure}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (result.AIReasoning is not null)
|
||||
{
|
||||
sb.AppendLine("AI Reasoning:");
|
||||
sb.AppendLine(result.AIReasoning);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
await this.AppendAsync(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the final summary section and elapsed time to the log file.
|
||||
/// </summary>
|
||||
public async Task WriteSummaryAsync(
|
||||
IReadOnlyList<VerificationResult> orderedResults,
|
||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
||||
TimeSpan elapsed)
|
||||
{
|
||||
var passCount = orderedResults.Count(r => r.Passed);
|
||||
var failCount = orderedResults.Count(r => !r.Passed);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(new string('═', 72));
|
||||
sb.AppendLine("SUMMARY");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var result in orderedResults)
|
||||
{
|
||||
sb.AppendLine($" {(result.Passed ? "✓" : "✗")} {result.SampleName}: {result.Summary}");
|
||||
}
|
||||
|
||||
foreach (var (name, reason) in skipped)
|
||||
{
|
||||
sb.AppendLine($" ○ {name}: Skipped — {reason}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Results: {passCount} passed{(failCount > 0 ? $", {failCount} failed" : "")}{(skipped.Count > 0 ? $", {skipped.Count} skipped" : "")}");
|
||||
sb.AppendLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
||||
|
||||
await this.AppendAsync(sb.ToString());
|
||||
}
|
||||
|
||||
private async Task AppendAsync(string text)
|
||||
{
|
||||
await this._writeLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await File.AppendAllTextAsync(this._path, text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._writeLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This tool runs the 01-get-started, 02-agents, and 03-workflows samples and verifies their output.
|
||||
// Deterministic samples are verified with exact string matching.
|
||||
// Non-deterministic (LLM) samples are verified using an agent-framework agent.
|
||||
//
|
||||
// Usage:
|
||||
// dotnet run # Run all samples
|
||||
// dotnet run -- 01_hello_agent 05_first_workflow # Run specific samples by name
|
||||
// dotnet run -- --category 01-get-started # Run the 01-get-started category
|
||||
// dotnet run -- --category 02-agents # Run the 02-agents category
|
||||
// dotnet run -- --category 03-workflows # Run the 03-workflows category
|
||||
// dotnet run -- --parallel 16 # Run up to 16 samples concurrently
|
||||
// dotnet run -- --log results.log # Write sequential log to file
|
||||
// dotnet run -- --csv results.csv # Write CSV summary to file
|
||||
//
|
||||
// Required environment variables (for AI-powered samples):
|
||||
// AZURE_OPENAI_ENDPOINT
|
||||
// AZURE_OPENAI_DEPLOYMENT_NAME (optional, defaults to gpt-5-mini)
|
||||
|
||||
using System.Diagnostics;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using VerifySamples;
|
||||
|
||||
var options = VerifyOptions.Parse(args);
|
||||
if (options is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
// Resolve the dotnet/ root directory (verify-samples is at dotnet/eng/verify-samples/)
|
||||
var dotnetRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
if (!File.Exists(Path.Combine(dotnetRoot, "agent-framework-dotnet.slnx")))
|
||||
{
|
||||
dotnetRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", ".."));
|
||||
}
|
||||
|
||||
// Set up the AI verifier
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5-mini";
|
||||
|
||||
OpenAI.Chat.ChatClient? chatClient = null;
|
||||
if (!string.IsNullOrEmpty(endpoint))
|
||||
{
|
||||
chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
}
|
||||
|
||||
// Set up optional log file writer
|
||||
LogFileWriter? logWriter = null;
|
||||
if (options.LogFilePath is not null)
|
||||
{
|
||||
logWriter = new LogFileWriter(options.LogFilePath);
|
||||
await logWriter.WriteHeaderAsync();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Run all samples
|
||||
var reporter = new ConsoleReporter();
|
||||
var verifier = new SampleVerifier(chatClient);
|
||||
var orchestrator = new VerificationOrchestrator(verifier, reporter, dotnetRoot, TimeSpan.FromMinutes(3), logWriter);
|
||||
|
||||
var run = await orchestrator.RunAllAsync(options.Samples, options.MaxParallelism);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
// Print summary
|
||||
var orderedResults = run.SampleOrder
|
||||
.Where(run.Results.ContainsKey)
|
||||
.Select(name => run.Results[name])
|
||||
.ToList();
|
||||
|
||||
reporter.PrintSummary(orderedResults, run.Skipped, stopwatch.Elapsed);
|
||||
|
||||
// Write log file summary
|
||||
if (logWriter is not null)
|
||||
{
|
||||
await logWriter.WriteSummaryAsync(orderedResults, run.Skipped, stopwatch.Elapsed);
|
||||
Console.WriteLine($"Log written to: {options.LogFilePath}");
|
||||
}
|
||||
|
||||
// Write CSV summary
|
||||
if (options.CsvFilePath is not null)
|
||||
{
|
||||
await CsvResultWriter.WriteAsync(options.CsvFilePath, orderedResults, run.Skipped, options.Samples);
|
||||
Console.WriteLine($"CSV written to: {options.CsvFilePath}");
|
||||
}
|
||||
|
||||
return orderedResults.Any(r => !r.Passed) ? 1 : 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
logWriter?.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a sample to verify, including its expected output.
|
||||
/// </summary>
|
||||
internal sealed class SampleDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Display name for the sample (e.g., "01_hello_agent").
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Relative path from the dotnet/ directory to the sample project directory.
|
||||
/// </summary>
|
||||
public required string ProjectPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables that the sample requires for a meaningful run.
|
||||
/// The runner checks these before running and will skip the sample if any are unset,
|
||||
/// recording a skip reason that indicates which required variables are missing.
|
||||
/// </summary>
|
||||
public string[] RequiredEnvironmentVariables { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Environment variables that the sample can use but typically has fallbacks or defaults for.
|
||||
/// If these are not set, the sample might prompt or behave interactively, which could cause
|
||||
/// automated verification to hang. The runner checks these and skips the sample if they are unset
|
||||
/// to avoid non-deterministic or blocking behavior in automated runs.
|
||||
/// </summary>
|
||||
public string[] OptionalEnvironmentVariables { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// If set, the sample is skipped with this reason.
|
||||
/// Use only for structural reasons (e.g., web server, multi-process, needs external service).
|
||||
/// Do NOT use for missing environment variables — those are checked dynamically.
|
||||
/// </summary>
|
||||
public string? SkipReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Substrings that must appear in stdout for the sample to pass.
|
||||
/// Used for deterministic verification.
|
||||
/// </summary>
|
||||
public string[] MustContain { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Substrings that must not appear in stdout for the sample to pass.
|
||||
/// </summary>
|
||||
public string[] MustNotContain { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// If true, <see cref="MustContain"/> entries cover the entire expected output —
|
||||
/// no AI verification is needed.
|
||||
/// </summary>
|
||||
public bool IsDeterministic { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Natural-language description of what the sample output should look like.
|
||||
/// Used by the AI verifier for non-deterministic samples.
|
||||
/// Each entry describes one aspect of the expected output that should be verified.
|
||||
/// </summary>
|
||||
public string[] ExpectedOutputDescription { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Sequence of stdin inputs to feed to the sample process.
|
||||
/// Each entry is written as a line (followed by newline) to the process stdin.
|
||||
/// A <c>null</c> entry inserts a delay without writing anything.
|
||||
/// Inputs are sent with a short delay between each to allow the process to prompt.
|
||||
/// </summary>
|
||||
public string?[] Inputs { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Delay in milliseconds between each input line. Default is 2000ms.
|
||||
/// Increase for samples that need more time between prompts (e.g., LLM calls between inputs).
|
||||
/// </summary>
|
||||
public int InputDelayMs { get; init; } = 2000;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Result of running a sample process.
|
||||
/// </summary>
|
||||
internal sealed record SampleRunResult(
|
||||
string Stdout,
|
||||
string Stderr,
|
||||
int ExitCode,
|
||||
TimeSpan Elapsed);
|
||||
|
||||
/// <summary>
|
||||
/// Runs a sample project via <c>dotnet run</c> and captures its output.
|
||||
/// </summary>
|
||||
internal static class SampleRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs <c>dotnet run --framework net10.0</c> in the given project directory.
|
||||
/// </summary>
|
||||
public static Task<SampleRunResult> RunAsync(
|
||||
string projectPath,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> RunAsync(projectPath, "run --framework net10.0", timeout, inputs: null, inputDelayMs: 0, cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs <c>dotnet run --framework net10.0</c> with stdin inputs.
|
||||
/// </summary>
|
||||
public static Task<SampleRunResult> RunAsync(
|
||||
string projectPath,
|
||||
TimeSpan timeout,
|
||||
string?[]? inputs,
|
||||
int inputDelayMs = 2000,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> RunAsync(projectPath, "run --framework net10.0", timeout, inputs, inputDelayMs, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs an arbitrary <c>dotnet</c> command in the given working directory.
|
||||
/// </summary>
|
||||
public static async Task<SampleRunResult> RunAsync(
|
||||
string workingDirectory,
|
||||
string dotnetArgs,
|
||||
TimeSpan timeout,
|
||||
string?[]? inputs = null,
|
||||
int inputDelayMs = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = dotnetArgs,
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = inputs is { Length: > 0 },
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
using var process = new Process { StartInfo = psi };
|
||||
process.Start();
|
||||
|
||||
var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
|
||||
// Feed stdin inputs with delays if configured
|
||||
if (inputs is { Length: > 0 })
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
await Task.Delay(inputDelayMs, cancellationToken);
|
||||
if (input is not null)
|
||||
{
|
||||
await process.StandardInput.WriteLineAsync(input.AsMemory(), cancellationToken);
|
||||
await process.StandardInput.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
process.StandardInput.Close();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or ObjectDisposedException or OperationCanceledException)
|
||||
{
|
||||
// Process may have exited before all inputs were sent
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(timeout);
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Timeout — kill the process
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
return new SampleRunResult(
|
||||
Stdout: await stdoutTask,
|
||||
Stderr: $"TIMEOUT: Sample did not complete within {timeout.TotalSeconds}s.\n{await stderrTask}",
|
||||
ExitCode: -1,
|
||||
Elapsed: sw.Elapsed);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
return new SampleRunResult(
|
||||
Stdout: await stdoutTask,
|
||||
Stderr: await stderrTask,
|
||||
ExitCode: process.ExitCode,
|
||||
Elapsed: sw.Elapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies sample output using deterministic checks and an AI agent
|
||||
/// for non-deterministic output validation.
|
||||
/// </summary>
|
||||
internal sealed class SampleVerifier
|
||||
{
|
||||
private readonly AIAgent? _verifierAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a verifier. If <paramref name="chatClient"/> is provided,
|
||||
/// AI-based verification is available for non-deterministic samples.
|
||||
/// </summary>
|
||||
public SampleVerifier(ChatClient? chatClient = null)
|
||||
{
|
||||
if (chatClient is not null)
|
||||
{
|
||||
this._verifierAgent = chatClient.AsAIAgent(
|
||||
instructions: """
|
||||
You are a test output verifier. You will be given:
|
||||
1. The actual stdout output of a program
|
||||
2. A list of expectations about what the output should contain or demonstrate
|
||||
|
||||
Your job is to determine whether the actual output satisfies each expectation.
|
||||
Be reasonable — the output comes from an LLM so exact wording won't match, but the
|
||||
semantic intent should be clearly satisfied.
|
||||
""",
|
||||
name: "OutputVerifier");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the output of a sample run against its definition.
|
||||
/// </summary>
|
||||
public async Task<VerificationResult> VerifyAsync(SampleDefinition sample, SampleRunResult run)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
|
||||
// 1. Exit code check
|
||||
if (run.ExitCode != 0)
|
||||
{
|
||||
failures.Add($"Exit code was {run.ExitCode}, expected 0. Stderr: {Truncate(run.Stderr, 500)}");
|
||||
}
|
||||
|
||||
// 2. Must-contain checks
|
||||
foreach (var expected in sample.MustContain)
|
||||
{
|
||||
if (!run.Stdout.Contains(expected, StringComparison.Ordinal))
|
||||
{
|
||||
failures.Add($"Output missing expected substring: \"{expected}\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Must-not-contain checks
|
||||
foreach (var unexpected in sample.MustNotContain)
|
||||
{
|
||||
if (run.Stdout.Contains(unexpected, StringComparison.Ordinal))
|
||||
{
|
||||
failures.Add($"Output contains unexpected substring: \"{unexpected}\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. AI verification for non-deterministic samples
|
||||
string? aiReasoning = null;
|
||||
if (!sample.IsDeterministic && sample.ExpectedOutputDescription.Length > 0)
|
||||
{
|
||||
if (this._verifierAgent is null)
|
||||
{
|
||||
failures.Add("AI verification required but no AI agent configured (missing AZURE_OPENAI_ENDPOINT).");
|
||||
}
|
||||
else
|
||||
{
|
||||
var aiResult = await this.VerifyWithAIAsync(run.Stdout, sample.ExpectedOutputDescription);
|
||||
aiReasoning = aiResult.Reasoning;
|
||||
|
||||
foreach (var unmet in aiResult.UnmetExpectations)
|
||||
{
|
||||
failures.Add($"AI expectation not met: {unmet}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool passed = failures.Count == 0;
|
||||
return new VerificationResult
|
||||
{
|
||||
SampleName = sample.Name,
|
||||
Passed = passed,
|
||||
Summary = passed ? "All checks passed" : $"{failures.Count} check(s) failed",
|
||||
Failures = failures,
|
||||
AIReasoning = aiReasoning,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(string Reasoning, List<string> UnmetExpectations)> VerifyWithAIAsync(
|
||||
string actualOutput,
|
||||
string[] expectations)
|
||||
{
|
||||
var expectationList = string.Join("\n", expectations.Select((e, i) => $" {i + 1}. {e}"));
|
||||
var prompt = $"""
|
||||
Actual program output:
|
||||
---
|
||||
{Truncate(actualOutput, 4000)}
|
||||
---
|
||||
|
||||
Expectations to verify:
|
||||
{expectationList}
|
||||
|
||||
Does the output satisfy all expectations?
|
||||
""";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await this._verifierAgent!.RunAsync<AIVerificationResponse>(prompt);
|
||||
var result = response.Result;
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return ($"AI verification returned null result. Raw: {response.Text}", ["AI verification returned null result."]);
|
||||
}
|
||||
|
||||
var reasoning = result.Reasoning ?? "(no reasoning provided)";
|
||||
|
||||
// Collect unmet expectations as individual failures
|
||||
var unmet = new List<string>();
|
||||
if (result.ExpectationResults is { Count: > 0 })
|
||||
{
|
||||
foreach (var er in result.ExpectationResults.Where(er => !er.Met))
|
||||
{
|
||||
var detail = string.IsNullOrWhiteSpace(er.Detail) ? er.Expectation : $"{er.Expectation} — {er.Detail}";
|
||||
unmet.Add(detail ?? "Unknown expectation");
|
||||
}
|
||||
|
||||
// If the model flagged overall failure but all individual expectations were met,
|
||||
// still treat as failure using the overall reasoning.
|
||||
if (unmet.Count == 0 && !result.Pass)
|
||||
{
|
||||
unmet.Add(reasoning);
|
||||
}
|
||||
}
|
||||
else if (!result.Pass)
|
||||
{
|
||||
// Fallback: no per-expectation detail but overall pass is false
|
||||
unmet.Add(reasoning);
|
||||
}
|
||||
|
||||
return (reasoning, unmet);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ($"AI verification error: {ex.Message}", [$"AI verification error: {ex.Message}"]);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int maxLength)
|
||||
=> text.Length <= maxLength ? text : text[..maxLength] + "... (truncated)";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structured response from the AI verification agent.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by JSON deserialization via RunAsync<T>.")]
|
||||
internal sealed class AIVerificationResponse
|
||||
{
|
||||
/// <summary>Whether all expectations were met.</summary>
|
||||
[JsonPropertyName("pass")]
|
||||
public bool Pass { get; set; }
|
||||
|
||||
/// <summary>Brief explanation of the overall assessment.</summary>
|
||||
[JsonPropertyName("reasoning")]
|
||||
public string? Reasoning { get; set; }
|
||||
|
||||
/// <summary>Per-expectation results.</summary>
|
||||
[JsonPropertyName("expectation_results")]
|
||||
public List<ExpectationResult>? ExpectationResults { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result for an individual expectation check.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by JSON deserialization via RunAsync<T>.")]
|
||||
internal sealed class ExpectationResult
|
||||
{
|
||||
/// <summary>The expectation text that was evaluated.</summary>
|
||||
[JsonPropertyName("expectation")]
|
||||
public string? Expectation { get; set; }
|
||||
|
||||
/// <summary>Whether this expectation was met.</summary>
|
||||
[JsonPropertyName("met")]
|
||||
public bool Met { get; set; }
|
||||
|
||||
/// <summary>Detail about how the expectation was or was not met.</summary>
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates sample verification: filters, runs in parallel, and collects results.
|
||||
/// </summary>
|
||||
internal sealed class VerificationOrchestrator
|
||||
{
|
||||
private readonly SampleVerifier _verifier;
|
||||
private readonly ConsoleReporter _reporter;
|
||||
private readonly LogFileWriter? _logWriter;
|
||||
private readonly string _dotnetRoot;
|
||||
private readonly TimeSpan _timeout;
|
||||
|
||||
public VerificationOrchestrator(
|
||||
SampleVerifier verifier,
|
||||
ConsoleReporter reporter,
|
||||
string dotnetRoot,
|
||||
TimeSpan timeout,
|
||||
LogFileWriter? logWriter = null)
|
||||
{
|
||||
this._verifier = verifier;
|
||||
this._reporter = reporter;
|
||||
this._logWriter = logWriter;
|
||||
this._dotnetRoot = dotnetRoot;
|
||||
this._timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of running all samples through the orchestrator.
|
||||
/// </summary>
|
||||
internal sealed record RunAllResult(
|
||||
ConcurrentDictionary<string, VerificationResult> Results,
|
||||
List<(string Name, string Reason)> Skipped,
|
||||
List<string> SampleOrder);
|
||||
|
||||
/// <summary>
|
||||
/// Filters samples, runs the runnable ones in parallel, and returns all results.
|
||||
/// </summary>
|
||||
public async Task<RunAllResult> RunAllAsync(
|
||||
IReadOnlyList<SampleDefinition> samples,
|
||||
int maxParallelism)
|
||||
{
|
||||
var skipped = new List<(string Name, string Reason)>();
|
||||
var runnableSamples = new List<SampleDefinition>();
|
||||
var sampleOrder = new List<string>();
|
||||
|
||||
// Separate samples into skipped and runnable
|
||||
foreach (var sample in samples)
|
||||
{
|
||||
sampleOrder.Add(sample.Name);
|
||||
|
||||
if (sample.SkipReason is not null)
|
||||
{
|
||||
skipped.Add((sample.Name, sample.SkipReason));
|
||||
this._reporter.WriteLineWithPrefix(sample.Name, $"SKIPPED — {sample.SkipReason}", ConsoleColor.Yellow);
|
||||
|
||||
if (this._logWriter is not null)
|
||||
{
|
||||
await this._logWriter.WriteSkippedAsync(sample.Name, sample.SkipReason);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var missingRequired = sample.RequiredEnvironmentVariables
|
||||
.Where(v => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(v)))
|
||||
.ToList();
|
||||
|
||||
var missingOptional = sample.OptionalEnvironmentVariables
|
||||
.Where(v => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(v)))
|
||||
.ToList();
|
||||
|
||||
if (missingRequired.Count > 0 || missingOptional.Count > 0)
|
||||
{
|
||||
var reasons = new List<string>();
|
||||
if (missingRequired.Count > 0)
|
||||
{
|
||||
reasons.Add($"Missing required: {string.Join(", ", missingRequired)}");
|
||||
}
|
||||
|
||||
if (missingOptional.Count > 0)
|
||||
{
|
||||
reasons.Add($"Missing optional (would cause console prompt hang): {string.Join(", ", missingOptional)}");
|
||||
}
|
||||
|
||||
var skipReason = string.Join("; ", reasons);
|
||||
skipped.Add((sample.Name, skipReason));
|
||||
this._reporter.WriteLineWithPrefix(sample.Name, $"SKIPPED — {skipReason}", ConsoleColor.Yellow);
|
||||
|
||||
if (this._logWriter is not null)
|
||||
{
|
||||
await this._logWriter.WriteSkippedAsync(sample.Name, skipReason);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
runnableSamples.Add(sample);
|
||||
}
|
||||
|
||||
// Run samples in parallel
|
||||
var results = new ConcurrentDictionary<string, VerificationResult>();
|
||||
var semaphore = new SemaphoreSlim(maxParallelism);
|
||||
|
||||
this._reporter.WriteLineWithPrefix(
|
||||
"runner", $"Running {runnableSamples.Count} samples (max {maxParallelism} parallel)...");
|
||||
|
||||
try
|
||||
{
|
||||
var tasks = runnableSamples.Select(sample => this.RunSingleAsync(sample, results, semaphore)).ToArray();
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Dispose();
|
||||
}
|
||||
|
||||
return new RunAllResult(results, skipped, sampleOrder);
|
||||
}
|
||||
|
||||
private async Task RunSingleAsync(
|
||||
SampleDefinition sample,
|
||||
ConcurrentDictionary<string, VerificationResult> results,
|
||||
SemaphoreSlim semaphore)
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var log = new List<string>();
|
||||
log.Add($"[{sample.Name}] Running...");
|
||||
this._reporter.WriteLineWithPrefix(sample.Name, "Running...");
|
||||
|
||||
var projectPath = Path.Combine(this._dotnetRoot, sample.ProjectPath);
|
||||
var run = sample.Inputs.Length > 0
|
||||
? await SampleRunner.RunAsync(projectPath, this._timeout, sample.Inputs, sample.InputDelayMs)
|
||||
: await SampleRunner.RunAsync(projectPath, this._timeout);
|
||||
|
||||
log.Add($"[{sample.Name}] Completed ({run.Elapsed.TotalSeconds:F1}s, exit={run.ExitCode})");
|
||||
this._reporter.WriteLineWithPrefix(
|
||||
sample.Name, $"Completed ({run.Elapsed.TotalSeconds:F1}s, exit={run.ExitCode}). Verifying...");
|
||||
|
||||
var result = await this._verifier.VerifyAsync(sample, run);
|
||||
|
||||
if (result.Passed)
|
||||
{
|
||||
log.Add($"[{sample.Name}] PASSED");
|
||||
this._reporter.WriteLineWithPrefix(sample.Name, "PASSED", ConsoleColor.Green);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Add($"[{sample.Name}] FAILED");
|
||||
this._reporter.WriteLineWithPrefix(sample.Name, "FAILED", ConsoleColor.Red);
|
||||
foreach (var failure in result.Failures)
|
||||
{
|
||||
log.Add($"[{sample.Name}] ✗ {failure}");
|
||||
this._reporter.WriteLineWithPrefix(sample.Name, $" ✗ {failure}", ConsoleColor.Red);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.AIReasoning is not null)
|
||||
{
|
||||
log.Add($"[{sample.Name}] AI: {result.AIReasoning}");
|
||||
this._reporter.WriteLineWithPrefix(
|
||||
sample.Name, $" AI: {Truncate(result.AIReasoning, 300)}", ConsoleColor.DarkGray);
|
||||
}
|
||||
|
||||
var verificationResult = new VerificationResult
|
||||
{
|
||||
SampleName = result.SampleName,
|
||||
Passed = result.Passed,
|
||||
Summary = result.Summary,
|
||||
Failures = result.Failures,
|
||||
AIReasoning = result.AIReasoning,
|
||||
Stdout = run.Stdout,
|
||||
Stderr = run.Stderr,
|
||||
LogLines = log,
|
||||
};
|
||||
results[sample.Name] = verificationResult;
|
||||
|
||||
if (this._logWriter is not null)
|
||||
{
|
||||
await this._logWriter.WriteSampleResultAsync(verificationResult);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int maxLength)
|
||||
=> text.Length <= maxLength ? text : text[..maxLength] + "...";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// The result of verifying a single sample.
|
||||
/// </summary>
|
||||
internal sealed class VerificationResult
|
||||
{
|
||||
public required string SampleName { get; init; }
|
||||
public required bool Passed { get; init; }
|
||||
public required string Summary { get; init; }
|
||||
public List<string> Failures { get; init; } = [];
|
||||
public string? AIReasoning { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The sample's stdout output, captured for log file output.
|
||||
/// </summary>
|
||||
public string? Stdout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The sample's stderr output, captured for log file output.
|
||||
/// </summary>
|
||||
public string? Stderr { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-sample log lines, buffered during parallel execution
|
||||
/// and written sequentially to the log file.
|
||||
/// </summary>
|
||||
public List<string> LogLines { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed command-line options for the sample verification tool.
|
||||
/// </summary>
|
||||
internal sealed class VerifyOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of samples to run concurrently.
|
||||
/// </summary>
|
||||
public int MaxParallelism { get; init; } = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Path to write a CSV summary file, or <c>null</c> to skip.
|
||||
/// </summary>
|
||||
public string? CsvFilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to write a sequential log file, or <c>null</c> to skip.
|
||||
/// </summary>
|
||||
public string? LogFilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The filtered list of samples to process.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SampleDefinition> Samples { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// All known sample set registries, keyed by category name.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, IReadOnlyList<SampleDefinition>> s_sampleSets =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["01-get-started"] = GetStartedSamples.All,
|
||||
["02-agents"] = AgentsSamples.All,
|
||||
["03-workflows"] = WorkflowSamples.All,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parses command-line arguments and resolves the sample list.
|
||||
/// Returns <c>null</c> and writes to stderr if the arguments are invalid.
|
||||
/// </summary>
|
||||
public static VerifyOptions? Parse(string[] args)
|
||||
{
|
||||
var argList = args.ToList();
|
||||
|
||||
var categoryFilter = ExtractArg(argList, "--category");
|
||||
var logFilePath = ExtractArg(argList, "--log");
|
||||
var csvFilePath = ExtractArg(argList, "--csv");
|
||||
|
||||
int maxParallelism = 8;
|
||||
var parallelArg = ExtractArg(argList, "--parallel");
|
||||
if (parallelArg is not null && int.TryParse(parallelArg, out var p) && p > 0)
|
||||
{
|
||||
maxParallelism = p;
|
||||
}
|
||||
|
||||
HashSet<string>? nameFilter = null;
|
||||
if (argList.Count > 0)
|
||||
{
|
||||
nameFilter = argList.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Build the sample list
|
||||
IReadOnlyList<SampleDefinition> samples;
|
||||
if (categoryFilter is not null)
|
||||
{
|
||||
if (!s_sampleSets.TryGetValue(categoryFilter, out var categoryList))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"Unknown category '{categoryFilter}'. Available: {string.Join(", ", s_sampleSets.Keys)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
samples = categoryList;
|
||||
}
|
||||
else
|
||||
{
|
||||
samples = s_sampleSets.Values.SelectMany(s => s).ToList();
|
||||
}
|
||||
|
||||
if (nameFilter is not null)
|
||||
{
|
||||
samples = samples.Where(s => nameFilter.Contains(s.Name)).ToList();
|
||||
}
|
||||
|
||||
if (samples.Count == 0)
|
||||
{
|
||||
var allNames = s_sampleSets.Values.SelectMany(s => s).Select(s => s.Name);
|
||||
Console.Error.WriteLine($"No matching samples found. Available: {string.Join(", ", allNames)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VerifyOptions
|
||||
{
|
||||
MaxParallelism = maxParallelism,
|
||||
LogFilePath = logFilePath,
|
||||
CsvFilePath = csvFilePath,
|
||||
Samples = samples,
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ExtractArg(List<string> list, string flag)
|
||||
{
|
||||
var idx = list.IndexOf(flag);
|
||||
if (idx < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (idx + 1 >= list.Count)
|
||||
{
|
||||
Console.Error.WriteLine($"Missing value for {flag}.");
|
||||
list.RemoveAt(idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = list[idx + 1];
|
||||
list.RemoveRange(idx, 2);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the expected behavior for each sample in 03-workflows.
|
||||
/// </summary>
|
||||
internal static class WorkflowSamples
|
||||
{
|
||||
public static IReadOnlyList<SampleDefinition> All { get; } =
|
||||
[
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// _StartHere
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_01_Streaming",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/01_Streaming",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"UppercaseExecutor: HELLO, WORLD!",
|
||||
"ReverseTextExecutor: !DLROW ,OLLEH",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_02_AgentsInWorkflows",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/02_AgentsInWorkflows",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show agent responses from a translation workflow.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_03_AgentWorkflowPatterns",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/03_AgentWorkflowPatterns",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
Inputs = ["sequential"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show a sequential workflow pattern with multiple agents executing tasks in order.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_04_MultiModelService",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/04_MultiModelService",
|
||||
RequiredEnvironmentVariables = ["BEDROCK_ACCESS_KEY", "BEDROCK_SECRET_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
||||
SkipReason = "Requires multiple external provider API keys (Bedrock, Anthropic, OpenAI).",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_05_SubWorkflows",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/05_SubWorkflows",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"=== Sub-Workflow Demonstration ===",
|
||||
"Final Output:",
|
||||
"=== Main Workflow Completed ===",
|
||||
"Sample Complete: Workflows can be composed hierarchically using sub-workflows",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_06_MixedWorkflowAgentsAndExecutors",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What is 2 plus 2?"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show agents and executors working together to process a user question.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_StartHere_07_WriterCriticWorkflow",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/07_WriterCriticWorkflow",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain = ["=== Writer-Critic Iteration Workflow ==="],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show a writer-critic iteration workflow with writer and critic sections.",
|
||||
"The critic should either approve or request revisions.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Agents
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Agents_CustomAgentExecutors",
|
||||
ProjectPath = "samples/03-workflows/Agents/CustomAgentExecutors",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show custom workflow events including slogan generation and feedback.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Agents_FoundryAgent",
|
||||
ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Requires Azure AI Foundry project endpoint.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Agents_GroupChatToolApproval",
|
||||
ProjectPath = "samples/03-workflows/Agents/GroupChatToolApproval",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain = ["Starting group chat workflow for software deployment..."],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show a group chat workflow with QA and DevOps agents for software deployment.",
|
||||
"There should be approval requests for tool calls.",
|
||||
"The workflow should show interaction between QA and DevOps agents toward deployment.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Agents_WorkflowAsAnAgent",
|
||||
ProjectPath = "samples/03-workflows/Agents/WorkflowAsAnAgent",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
Inputs = ["hello", "exit"],
|
||||
InputDelayMs = 5000,
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show a conversational workflow responding to the user's hello message.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Checkpoint
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Checkpoint_CheckpointAndRehydrate",
|
||||
ProjectPath = "samples/03-workflows/Checkpoint/CheckpointAndRehydrate",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"Workflow completed with result:",
|
||||
"Number of checkpoints created:",
|
||||
"Hydrating a new workflow instance from the 6th checkpoint.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Checkpoint_CheckpointAndResume",
|
||||
ProjectPath = "samples/03-workflows/Checkpoint/CheckpointAndResume",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"Workflow completed with result:",
|
||||
"Number of checkpoints created:",
|
||||
"Restoring from the 6th checkpoint.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Checkpoint_CheckpointWithHumanInTheLoop",
|
||||
ProjectPath = "samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop",
|
||||
RequiredEnvironmentVariables = [],
|
||||
Inputs = ["50", "25", "40", "45", "42", "50", "25", "40", "45", "42"],
|
||||
InputDelayMs = 1000,
|
||||
MustContain = ["found in"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show a number guessing game with higher/lower hints that eventually reaches the correct number.",
|
||||
"The output should demonstrate checkpoint save and restore behavior.",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Concurrent
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Concurrent_Concurrent",
|
||||
ProjectPath = "samples/03-workflows/Concurrent/Concurrent",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show results from concurrent agent processing.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Concurrent_MapReduce",
|
||||
ProjectPath = "samples/03-workflows/Concurrent/MapReduce",
|
||||
RequiredEnvironmentVariables = [],
|
||||
MustContain =
|
||||
[
|
||||
"=== RUNNING WORKFLOW ===",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// ConditionalEdges
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_ConditionalEdges_01_EdgeCondition",
|
||||
ProjectPath = "samples/03-workflows/ConditionalEdges/01_EdgeCondition",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an email being classified as spam or not spam and processed accordingly.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_ConditionalEdges_02_SwitchCase",
|
||||
ProjectPath = "samples/03-workflows/ConditionalEdges/02_SwitchCase",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an ambiguous email being classified as spam, not spam, or uncertain.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_ConditionalEdges_03_MultiSelection",
|
||||
ProjectPath = "samples/03-workflows/ConditionalEdges/03_MultiSelection",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an email being classified and potentially routed to multiple handlers.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// HumanInTheLoop
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_HumanInTheLoop_Basic",
|
||||
ProjectPath = "samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic",
|
||||
RequiredEnvironmentVariables = [],
|
||||
Inputs = ["50", "25", "40", "45", "42"],
|
||||
InputDelayMs = 1000,
|
||||
MustContain = ["found in"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show a number guessing game with higher/lower hints that eventually reaches the correct number 42.",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Loop
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Loop",
|
||||
ProjectPath = "samples/03-workflows/Loop",
|
||||
RequiredEnvironmentVariables = [],
|
||||
MustContain = ["Result:"],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// SharedStates
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_SharedStates",
|
||||
ProjectPath = "samples/03-workflows/SharedStates",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"Total Paragraphs:",
|
||||
"Total Words:",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Visualization
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Visualization",
|
||||
ProjectPath = "samples/03-workflows/Visualization",
|
||||
RequiredEnvironmentVariables = [],
|
||||
IsDeterministic = true,
|
||||
MustContain =
|
||||
[
|
||||
"Generating workflow visualization...",
|
||||
"Mermaid string:",
|
||||
"DiGraph string:",
|
||||
],
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Observability
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Observability_ApplicationInsights",
|
||||
ProjectPath = "samples/03-workflows/Observability/ApplicationInsights",
|
||||
RequiredEnvironmentVariables = ["APPLICATIONINSIGHTS_CONNECTION_STRING"],
|
||||
SkipReason = "Requires Application Insights connection string.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Observability_AspireDashboard",
|
||||
ProjectPath = "samples/03-workflows/Observability/AspireDashboard",
|
||||
RequiredEnvironmentVariables = [],
|
||||
SkipReason = "Requires Aspire Dashboard / OTLP endpoint.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Observability_WorkflowAsAnAgent",
|
||||
ProjectPath = "samples/03-workflows/Observability/WorkflowAsAnAgent",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Interactive console with ReadLine loop; requires OTLP endpoint.",
|
||||
},
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Declarative
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_ConfirmInput",
|
||||
ProjectPath = "samples/03-workflows/Declarative/ConfirmInput",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
Inputs = ["hello", "hello"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a confirmation prompt and a user response."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_CustomerSupport",
|
||||
ProjectPath = "samples/03-workflows/Declarative/CustomerSupport",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["My laptop won't start"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription = ["The output should show a customer support workflow processing a laptop issue, with agent responses providing troubleshooting or support."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_DeepResearch",
|
||||
ProjectPath = "samples/03-workflows/Declarative/DeepResearch",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Requires external weather API (wttr.in).",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_ExecuteCode",
|
||||
ProjectPath = "samples/03-workflows/Declarative/ExecuteCode",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
Inputs = ["What is 12 * 34?"],
|
||||
InputDelayMs = 5000,
|
||||
ExpectedOutputDescription = ["The output should show a declarative workflow executing generated code, processing a math question and producing a result."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_ExecuteWorkflow",
|
||||
ProjectPath = "samples/03-workflows/Declarative/ExecuteWorkflow",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
SkipReason = "Requires a workflow file path as a CLI argument.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_FunctionTools",
|
||||
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What are today's specials?", "EXIT"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_GenerateCode",
|
||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
||||
IsDeterministic = true,
|
||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_HostedWorkflow",
|
||||
ProjectPath = "samples/03-workflows/Declarative/HostedWorkflow",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
SkipReason = "Hosts a persistent workflow server that does not exit.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_InputArguments",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InputArguments",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["I'd like to visit Seattle", "EXIT"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow capturing location input and providing travel-related information about Seattle."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeFunctionTool",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InvokeFunctionTool",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What's the soup of the day?", "EXIT"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow invoking a function tool (e.g. a menu plugin) to answer a question about the soup of the day."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeMcpTool",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InvokeMcpTool",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["Search for .NET tutorials on Microsoft Learn"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow using MCP tools to search Microsoft Learn documentation and provide a summary of results."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_Marketing",
|
||||
ProjectPath = "samples/03-workflows/Declarative/Marketing",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["A smart water bottle that tracks hydration"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription = ["The output should show a marketing workflow generating content about a smart water bottle product."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_StudentTeacher",
|
||||
ProjectPath = "samples/03-workflows/Declarative/StudentTeacher",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What is 18 + 27?"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription = ["The output should show a student-teacher workflow where a student asks a math question and a teacher provides the answer."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_ToolApproval",
|
||||
ProjectPath = "samples/03-workflows/Declarative/ToolApproval",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
Inputs = ["Search for .NET tutorials", "EXIT"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow using an MCP tool with approval to search Microsoft Learn, followed by an exit from the input loop."],
|
||||
},
|
||||
];
|
||||
}
|
||||
+6
-7
@@ -1,25 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<!-- This is a top-level console app; ConfigureAwait is unnecessary -->
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Evaluation" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Evaluation.Quality" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Evaluation.Safety" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -2,17 +2,19 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<RCNumber>4</RCNumber>
|
||||
<RCNumber>5</RCNumber>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260311.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260311.1</PackageVersion>
|
||||
<GitTag>1.0.0-rc4</GitTag>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260330.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260330.1</PackageVersion>
|
||||
<GitTag>1.0.0-rc5</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
|
||||
<PackageValidationBaselineVersion>0.0.1</PackageValidationBaselineVersion>
|
||||
<PackageValidationBaselineVersion>1.0.0-rc4</PackageValidationBaselineVersion>
|
||||
<!-- Enable validation for RC packages and GA packages -->
|
||||
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsGenerallyAvailable)' == 'true'">true</EnablePackageValidation>
|
||||
<!-- Validate assembly attributes only for Publish builds -->
|
||||
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
|
||||
<!-- Do not validate reference assemblies -->
|
||||
|
||||
@@ -70,7 +70,7 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
|
||||
|
||||
if (approvalRequest.AdditionalProperties != null)
|
||||
{
|
||||
approvalResponse.AdditionalProperties = new AdditionalPropertiesDictionary();
|
||||
approvalResponse.AdditionalProperties = [];
|
||||
foreach (var kvp in approvalRequest.AdditionalProperties)
|
||||
{
|
||||
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
|
||||
|
||||
+4
-4
@@ -131,9 +131,9 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
approvalCalls.Remove(functionResult.CallId);
|
||||
}
|
||||
else if (transformedContents != null)
|
||||
else
|
||||
{
|
||||
transformedContents.Add(content);
|
||||
transformedContents?.Add(content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +155,10 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
result.Add(newMessage);
|
||||
}
|
||||
else if (result != null)
|
||||
else
|
||||
{
|
||||
// We're already copying messages, so copy this unchanged message too
|
||||
result.Add(message);
|
||||
result?.Add(message);
|
||||
}
|
||||
// If result is null, we haven't made any changes yet, so keep processing
|
||||
}
|
||||
|
||||
+8
-20
@@ -57,16 +57,10 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
throw new InvalidOperationException("Invalid request_approval tool call");
|
||||
}
|
||||
|
||||
var request = toolCall.Arguments.TryGetValue("request", out var reqObj) &&
|
||||
var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) &&
|
||||
reqObj is JsonElement argsElement &&
|
||||
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
|
||||
approvalRequest != null ? approvalRequest : null;
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize approval request from tool call");
|
||||
}
|
||||
|
||||
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
|
||||
return new ToolApprovalRequestContent(
|
||||
requestId: request.ApprovalId,
|
||||
new FunctionCallContent(
|
||||
@@ -77,17 +71,11 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
|
||||
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
var approvalResponse = result.Result is JsonElement je ?
|
||||
var approvalResponse = (result.Result is JsonElement je ?
|
||||
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||
result.Result is string str ?
|
||||
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||
result.Result as ApprovalResponse;
|
||||
|
||||
if (approvalResponse == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize approval response from tool result");
|
||||
}
|
||||
|
||||
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
|
||||
return approval.CreateResponse(approvalResponse.Approved);
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
@@ -121,7 +109,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
// Track approval ID to original call ID mapping
|
||||
_ = new Dictionary<string, string>();
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = new(); // Remote approvals
|
||||
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = []; // Remote approvals
|
||||
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
var message = messages[messageIndex];
|
||||
@@ -146,7 +134,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
});
|
||||
}
|
||||
else if (content is FunctionResultContent toolResult &&
|
||||
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval) == true)
|
||||
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
|
||||
{
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
||||
@@ -161,9 +149,9 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
});
|
||||
}
|
||||
else if (result != null)
|
||||
else
|
||||
{
|
||||
result.Add(message);
|
||||
result?.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,9 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
|
||||
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
|
||||
{
|
||||
// Deserialize the state
|
||||
TState? newState = JsonSerializer.Deserialize(
|
||||
if (JsonSerializer.Deserialize(
|
||||
dataContent.Data.Span,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) as TState;
|
||||
if (newState != null)
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
|
||||
{
|
||||
this.State = newState;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using OpenTelemetry.Trace;
|
||||
|
||||
#region Setup Telemetry
|
||||
|
||||
// Source name for this sample's custom ActivitySource and Meter; other instrumentation uses their own sources/categories.
|
||||
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
|
||||
const string ServiceName = "AgentOpenTelemetry";
|
||||
|
||||
@@ -40,7 +41,6 @@ var resource = ResourceBuilder.CreateDefault()
|
||||
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
||||
.AddSource(SourceName) // Our custom activity source
|
||||
.AddSource("*Microsoft.Agents.AI") // Agent Framework telemetry
|
||||
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
|
||||
|
||||
@@ -54,8 +54,7 @@ using var tracerProvider = tracerProviderBuilder.Build();
|
||||
// Setup metrics with resource and instrument name filtering
|
||||
using var meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
||||
.AddMeter(SourceName) // Our custom meter
|
||||
.AddMeter("*Microsoft.Agents.AI") // Agent Framework metrics
|
||||
.AddMeter(SourceName) // Our custom meter source
|
||||
.AddHttpClientInstrumentation() // HTTP client metrics
|
||||
.AddRuntimeInstrumentation() // .NET runtime metrics
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
|
||||
@@ -128,7 +127,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
|
||||
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
||||
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
|
||||
.AsBuilder()
|
||||
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
||||
.Build();
|
||||
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
@@ -5,8 +5,8 @@ This sample demonstrates how to create an AIAgent using Anthropic Claude models
|
||||
The sample supports three deployment scenarios:
|
||||
|
||||
1. **Anthropic Public API** - Direct connection to Anthropic's public API
|
||||
2. **Azure Foundry with API Key** - Anthropic models deployed through Azure Foundry using API key authentication
|
||||
3. **Azure Foundry with Azure CLI** - Anthropic models deployed through Azure Foundry using Azure CLI credentials
|
||||
2. **Microsoft Foundry with API Key** - Anthropic models deployed through Microsoft Foundry using API key authentication
|
||||
3. **Microsoft Foundry with Azure CLI** - Anthropic models deployed through Microsoft Foundry using Azure CLI credentials
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -25,29 +25,29 @@ $env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic A
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||
```
|
||||
|
||||
### For Azure Foundry with API Key
|
||||
### For Microsoft Foundry with API Key
|
||||
|
||||
- Azure Foundry service endpoint and deployment configured
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Anthropic API key
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure Foundry resource name (subdomain before .services.ai.azure.com)
|
||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Microsoft Foundry resource name (subdomain before .services.ai.azure.com)
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||
```
|
||||
|
||||
### For Azure Foundry with Azure CLI
|
||||
### For Microsoft Foundry with Azure CLI
|
||||
|
||||
- Azure Foundry service endpoint and deployment configured
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure Foundry resource name (subdomain before .services.ai.azure.com)
|
||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Microsoft Foundry resource name (subdomain before .services.ai.azure.com)
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||
```
|
||||
|
||||
**Note**: When using Azure Foundry with Azure CLI, make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
**Note**: When using Microsoft Foundry with Azure CLI, make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete - sample uses deprecated PersistentAgentsClientExtensions
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend.
|
||||
// This sample shows how to create and use a simple AI agent with Microsoft Foundry Agents as the backend.
|
||||
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
|
||||
+3
-3
@@ -13,14 +13,14 @@ Below is a comparison between the classic and new Foundry Agents approaches:
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure Foundry service endpoint and deployment configured
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
|
||||
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend.
|
||||
// This sample shows how to create and use AI agents with Microsoft Foundry Agents as the backend.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AzureAI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
|
||||
// Get a client to create/retrieve/delete server side agents with Microsoft Foundry Agents.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
@@ -30,14 +31,18 @@ var createdAgentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: J
|
||||
// agentVersion.Name = <agentName>
|
||||
|
||||
// You can use an AIAgent with an already created server side agent version.
|
||||
AIAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
|
||||
FoundryAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
|
||||
|
||||
// You can also create another AIAgent version by providing the same name with a different definition.
|
||||
AIAgent newJokerAgent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: "You are extremely hilarious at telling jokes.");
|
||||
AgentVersion newJokerAgentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
JokerName,
|
||||
new AgentVersionCreationOptions(new PromptAgentDefinition(model: deploymentName) { Instructions = "You are extremely hilarious at telling jokes." }));
|
||||
FoundryAgent newJokerAgent = aiProjectClient.AsAIAgent(newJokerAgentVersion);
|
||||
|
||||
// You can also get the AIAgent latest version just providing its name.
|
||||
AIAgent jokerAgentLatest = await aiProjectClient.GetAIAgentAsync(name: JokerName);
|
||||
var latestAgentVersion = jokerAgentLatest.GetService<AgentVersion>()!;
|
||||
AgentRecord jokerAgentRecord = await aiProjectClient.Agents.GetAgentAsync(JokerName);
|
||||
FoundryAgent jokerAgentLatest = aiProjectClient.AsAIAgent(jokerAgentRecord);
|
||||
AgentVersion latestAgentVersion = jokerAgentRecord.GetLatestVersion();
|
||||
|
||||
// The AIAgent version can be accessed via the GetService method.
|
||||
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
|
||||
|
||||
@@ -13,14 +13,14 @@ Below is a comparison between the classic and new Foundry Agents approaches:
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure Foundry service endpoint and deployment configured
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
|
||||
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Azure AI Foundry.
|
||||
// You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI or any other model you have deployed in your Azure AI Foundry resource.
|
||||
// This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Microsoft Foundry.
|
||||
// You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI or any other model you have deployed in your Microsoft Foundry resource.
|
||||
// Note: Ensure that you pick a model that suits your needs. For example, if you want to use function calling, ensure that the model you pick supports function calling.
|
||||
|
||||
using System.ClientModel;
|
||||
@@ -15,7 +15,7 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? th
|
||||
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
var model = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "Phi-4-mini-instruct";
|
||||
|
||||
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Azure Foundry.
|
||||
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Microsoft Foundry.
|
||||
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
|
||||
|
||||
// Create the OpenAI client with either an API key or Azure CLI credential.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Overview
|
||||
|
||||
This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Azure AI Foundry.
|
||||
This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Microsoft Foundry.
|
||||
|
||||
You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI or any other model you have deployed in Azure AI Foundry.
|
||||
You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI or any other model you have deployed in Microsoft Foundry.
|
||||
|
||||
**Note**: Ensure that you pick a model that suits your needs. For example, if you want to use function calling, ensure that the model you pick supports function calling.
|
||||
|
||||
@@ -11,19 +11,19 @@ You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI o
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure AI Foundry resource
|
||||
- A model deployment in your Azure AI Foundry resource. This example defaults to using the `Phi-4-mini-instruct` model,
|
||||
- Microsoft Foundry resource
|
||||
- A model deployment in your Microsoft Foundry resource. This example defaults to using the `Phi-4-mini-instruct` model,
|
||||
so if you want to use a different model, ensure that you set your `AZURE_AI_MODEL_DEPLOYMENT_NAME` environment
|
||||
variable to the name of your deployed model.
|
||||
- An API key or role based authentication to access the Azure AI Foundry resource
|
||||
- An API key or role based authentication to access the Microsoft Foundry resource
|
||||
|
||||
See [here](https://learn.microsoft.com/en-us/azure/ai-foundry/quickstarts/get-started-code?tabs=csharp) for more info on setting up these prerequisites
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
# Replace with your Azure AI Foundry resource endpoint
|
||||
# Ensure that you have the "/openai/v1/" path in the URL, since this is required when using the OpenAI SDK to access Azure Foundry models.
|
||||
# Replace with your Microsoft Foundry resource endpoint
|
||||
# Ensure that you have the "/openai/v1/" path in the URL, since this is required when using the OpenAI SDK to access Microsoft Foundry models.
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://ai-foundry-<myresourcename>.services.ai.azure.com/openai/v1/"
|
||||
|
||||
# Optional, defaults to using Azure CLI for authentication if not provided
|
||||
|
||||
@@ -18,7 +18,7 @@ See the README.md for each sample for the prerequisites for that sample.
|
||||
|[Creating an AIAgent with Anthropic](./Agent_With_Anthropic/)|This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service|
|
||||
|[Creating an AIAgent with Foundry Agents using Azure.AI.Agents.Persistent](./Agent_With_AzureAIAgentsPersistent/)|This sample demonstrates how to create a Foundry Persistent agent and expose it as an AIAgent using the Azure.AI.Agents.Persistent SDK|
|
||||
|[Creating an AIAgent with Foundry Agents using Azure.AI.Project](./Agent_With_AzureAIProject/)|This sample demonstrates how to create an Foundry Project agent and expose it as an AIAgent using the Azure.AI.Project SDK|
|
||||
|[Creating an AIAgent with AzureFoundry Model](./Agent_With_AzureFoundryModel/)|This sample demonstrates how to use any model deployed to Azure Foundry to create an AIAgent|
|
||||
|[Creating an AIAgent with Foundry Model](./Agent_With_AzureFoundryModel/)|This sample demonstrates how to use any model deployed to Microsoft Foundry to create an AIAgent|
|
||||
|[Creating an AIAgent with Azure OpenAI ChatCompletion](./Agent_With_AzureOpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using Azure OpenAI ChatCompletion as the underlying inference service|
|
||||
|[Creating an AIAgent with Azure OpenAI Responses](./Agent_With_AzureOpenAIResponses/)|This sample demonstrates how to create an AIAgent using Azure OpenAI Responses as the underlying inference service|
|
||||
|[Creating an AIAgent with a custom implementation](./Agent_With_CustomImplementation/)|This sample demonstrates how to create an AIAgent with a custom implementation|
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use Agent Skills with a ChatClientAgent.
|
||||
// Agent Skills are modular packages of instructions and resources that extend an agent's capabilities.
|
||||
// Skills follow the progressive disclosure pattern: advertise -> load -> read resources.
|
||||
//
|
||||
// This sample includes the expense-report skill:
|
||||
// - Policy-based expense filing with references and assets
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// --- Skills Provider ---
|
||||
// Discovers skills from the 'skills' directory and makes them available to the agent
|
||||
var skillsProvider = new FileAgentSkillsProvider(skillPath: Path.Combine(AppContext.BaseDirectory, "skills"));
|
||||
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "SkillsAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example 1: Expense policy question (loads FAQ resource) ---
|
||||
Console.WriteLine("Example 1: Checking expense policy FAQ");
|
||||
Console.WriteLine("---------------------------------------");
|
||||
AgentResponse response1 = await agent.RunAsync("Are tips reimbursable? I left a 25% tip on a taxi ride and want to know if that's covered.");
|
||||
Console.WriteLine($"Agent: {response1.Text}\n");
|
||||
|
||||
// --- Example 2: Filing an expense report (multi-turn with template asset) ---
|
||||
Console.WriteLine("Example 2: Filing an expense report");
|
||||
Console.WriteLine("---------------------------------------");
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
AgentResponse response2 = await agent.RunAsync("I had 3 client dinners and a $1,200 flight last week. Return a draft expense report and ask about any missing details.",
|
||||
session);
|
||||
Console.WriteLine($"Agent: {response2.Text}\n");
|
||||
@@ -1,63 +0,0 @@
|
||||
# Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to use **Agent Skills** with a `ChatClientAgent` in the Microsoft Agent Framework.
|
||||
|
||||
## What are Agent Skills?
|
||||
|
||||
Agent Skills are modular packages of instructions and resources that enable AI agents to perform specialized tasks. They follow the [Agent Skills specification](https://agentskills.io/) and implement the progressive disclosure pattern:
|
||||
|
||||
1. **Advertise**: Skills are advertised with name + description (~100 tokens per skill)
|
||||
2. **Load**: Full instructions are loaded on-demand via `load_skill` tool
|
||||
3. **Resources**: References and other files loaded via `read_skill_resource` tool
|
||||
|
||||
## Skills Included
|
||||
|
||||
### expense-report
|
||||
Policy-based expense filing with spending limits, receipt requirements, and approval workflows.
|
||||
- `references/POLICY_FAQ.md` — Detailed expense policy Q&A
|
||||
- `assets/expense-report-template.md` — Submission template
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Agent_Step01_BasicSkills/
|
||||
├── Program.cs
|
||||
├── Agent_Step01_BasicSkills.csproj
|
||||
└── skills/
|
||||
└── expense-report/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ └── POLICY_FAQ.md
|
||||
└── assets/
|
||||
└── expense-report-template.md
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model
|
||||
|
||||
### Setup
|
||||
1. Set environment variables:
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
2. Run the sample:
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
The sample runs two examples:
|
||||
|
||||
1. **Expense policy FAQ** — Asks about tip reimbursement; the agent loads the expense-report skill and reads the FAQ resource
|
||||
2. **Filing an expense report** — Multi-turn conversation to draft an expense report using the template asset
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
- [Microsoft Agent Framework Documentation](../../../../../docs/)
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: expense-report
|
||||
description: File and validate employee expense reports according to Contoso company policy. Use when asked about expense submissions, reimbursement rules, receipt requirements, spending limits, or expense categories.
|
||||
metadata:
|
||||
author: contoso-finance
|
||||
version: "2.1"
|
||||
---
|
||||
|
||||
# Expense Report
|
||||
|
||||
## Categories and Limits
|
||||
|
||||
| Category | Limit | Receipt | Approval |
|
||||
|---|---|---|---|
|
||||
| Meals — solo | $50/day | >$25 | No |
|
||||
| Meals — team/client | $75/person | Always | Manager if >$200 total |
|
||||
| Lodging | $250/night | Always | Manager if >3 nights |
|
||||
| Ground transport | $100/day | >$15 | No |
|
||||
| Airfare | Economy | Always | Manager; VP if >$1,500 |
|
||||
| Conference/training | $2,000/event | Always | Manager + L&D |
|
||||
| Office supplies | $100 | Yes | No |
|
||||
| Software/subscriptions | $50/month | Yes | Manager if >$200/year |
|
||||
|
||||
## Filing Process
|
||||
|
||||
1. Collect receipts — must show vendor, date, amount, payment method.
|
||||
2. Categorize per table above.
|
||||
3. Use template: [assets/expense-report-template.md](assets/expense-report-template.md).
|
||||
4. For client/team meals: list attendee names and business purpose.
|
||||
5. Submit — auto-approved if <$500; manager if $500–$2,000; VP if >$2,000.
|
||||
6. Reimbursement: 10 business days via direct deposit.
|
||||
|
||||
## Policy Rules
|
||||
|
||||
- Submit within 30 days of transaction.
|
||||
- Alcohol is never reimbursable.
|
||||
- Foreign currency: convert to USD at transaction-date rate; note original currency and amount.
|
||||
- Mixed personal/business travel: only business portion reimbursable; provide comparison quotes.
|
||||
- Lost receipts (>$25): file Lost Receipt Affidavit from Finance. Max 2 per quarter.
|
||||
- For policy questions not covered above, consult the FAQ: [references/POLICY_FAQ.md](references/POLICY_FAQ.md). Answers should be based on what this document and the FAQ state.
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
# Expense Report Template
|
||||
|
||||
| Date | Category | Vendor | Description | Amount (USD) | Original Currency | Original Amount | Attendees | Business Purpose | Receipt Attached |
|
||||
|------|----------|--------|-------------|--------------|-------------------|-----------------|-----------|------------------|------------------|
|
||||
| | | | | | | | | | Yes or No |
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
# Expense Policy — Frequently Asked Questions
|
||||
|
||||
## Meals
|
||||
|
||||
**Q: Can I expense coffee or snacks during the workday?**
|
||||
A: Daily coffee/snacks under $10 are not reimbursable (considered personal). Coffee purchased during a client meeting or team working session is reimbursable as a team meal.
|
||||
|
||||
**Q: What if a team dinner exceeds the per-person limit?**
|
||||
A: The $75/person limit applies as a guideline. Overages up to 20% are accepted with a written justification (e.g., "client dinner at venue chosen by client"). Overages beyond 20% require pre-approval from your VP.
|
||||
|
||||
**Q: Do I need to list every attendee?**
|
||||
A: Yes. For client meals, list the client's name and company. For team meals, list all employee names. For groups over 10, you may attach a separate attendee list.
|
||||
|
||||
## Travel
|
||||
|
||||
**Q: Can I book a premium economy or business class flight?**
|
||||
A: Economy class is the standard. Premium economy is allowed for flights over 6 hours. Business class requires VP pre-approval and is generally reserved for flights over 10 hours or medical accommodation.
|
||||
|
||||
**Q: What about ride-sharing (Uber/Lyft) vs. rental cars?**
|
||||
A: Use ride-sharing for trips under 30 miles round-trip. Rent a car for multi-day travel or when ride-sharing would exceed $100/day. Always choose the compact/standard category unless traveling with 3+ people.
|
||||
|
||||
**Q: Are tips reimbursable?**
|
||||
A: Tips up to 20% are reimbursable for meals, taxi/ride-share, and hotel housekeeping. Tips above 20% require justification.
|
||||
|
||||
## Lodging
|
||||
|
||||
**Q: What if the $250/night limit isn't enough for the city I'm visiting?**
|
||||
A: For high-cost cities (New York, San Francisco, London, Tokyo, Sydney), the limit is automatically increased to $350/night. No additional approval is needed. For other locations where rates are unusually high (e.g., during a major conference), request a per-trip exception from your manager before booking.
|
||||
|
||||
**Q: Can I stay with friends/family instead and get a per-diem?**
|
||||
A: No. Contoso reimburses actual lodging costs only, not per-diems.
|
||||
|
||||
## Subscriptions and Software
|
||||
|
||||
**Q: Can I expense a personal productivity tool?**
|
||||
A: Software must be directly related to your job function. Tools like IDE licenses, design software, or project management apps are reimbursable. General productivity apps (note-taking, personal calendar) are not, unless your manager confirms a business need in writing.
|
||||
|
||||
**Q: What about annual subscriptions?**
|
||||
A: Annual subscriptions over $200 require manager approval before purchase. Submit the approval email with your expense report.
|
||||
|
||||
## Receipts and Documentation
|
||||
|
||||
**Q: My receipt is faded/damaged. What do I do?**
|
||||
A: Try to obtain a duplicate from the vendor. If not possible, submit a Lost Receipt Affidavit (available from the Finance SharePoint site). You're limited to 2 affidavits per quarter.
|
||||
|
||||
**Q: Do I need a receipt for parking meters or tolls?**
|
||||
A: For amounts under $15, no receipt is required — just note the date, location, and amount. For $15 and above, a receipt or bank/credit card statement excerpt is required.
|
||||
|
||||
## Approval and Reimbursement
|
||||
|
||||
**Q: My manager is on leave. Who approves my report?**
|
||||
A: Expense reports can be approved by your skip-level manager or any manager designated as an alternate approver in the expense system.
|
||||
|
||||
**Q: Can I submit expenses from a previous quarter?**
|
||||
A: The standard 30-day window applies. Expenses older than 30 days require a written explanation and VP approval. Expenses older than 90 days are not reimbursable except in extraordinary circumstances (extended leave, medical emergency) with CFO approval.
|
||||
+4
@@ -14,6 +14,10 @@
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use file-based Agent Skills with a ChatClientAgent.
|
||||
// Skills are discovered from SKILL.md files on disk and follow the progressive disclosure pattern:
|
||||
// 1. Advertise — skill names and descriptions in the system prompt
|
||||
// 2. Load — full instructions loaded on demand via load_skill tool
|
||||
// 3. Read resources — reference files read via read_skill_resource tool
|
||||
// 4. Run scripts — scripts executed via run_skill_script tool with a subprocess executor
|
||||
//
|
||||
// This sample uses a unit-converter skill that converts between miles, kilometers, pounds, and kilograms.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// --- Skills Provider ---
|
||||
// Discovers skills from the 'skills' directory containing SKILL.md files.
|
||||
// The script runner runs file-based scripts (e.g. Python) as local subprocesses.
|
||||
var skillsProvider = new AgentSkillsProvider(
|
||||
Path.Combine(AppContext.BaseDirectory, "skills"),
|
||||
SubprocessScriptRunner.RunAsync);
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
Console.WriteLine("Converting units with file-based skills");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
@@ -0,0 +1,51 @@
|
||||
# File-Based Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to use **file-based Agent Skills** with a `ChatClientAgent`.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Discovering skills from `SKILL.md` files on disk via `AgentFileSkillsSource`
|
||||
- The progressive disclosure pattern: advertise → load → read resources → run scripts
|
||||
- Using the `AgentSkillsProvider` constructor with a skill directory path and script executor
|
||||
- Running file-based scripts (Python) via a subprocess-based executor
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter
|
||||
|
||||
Converts between common units (miles↔km, pounds↔kg) using a multiplication factor.
|
||||
|
||||
- `references/conversion-table.md` — Conversion factor table
|
||||
- `scripts/convert.py` — Python script that performs the conversion
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model
|
||||
- Python 3 installed and available as `python3` on your PATH
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
Converting units with file-based skills
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
```
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: unit-converter
|
||||
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a unit conversion:
|
||||
1. First, review `references/conversion-table.md` to find the correct factor
|
||||
2. Run the `scripts/convert.py` script with `--value <number> --factor <factor>` (e.g. `--value 26.2 --factor 1.60934`)
|
||||
3. Present the converted value clearly with both units
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Unit conversion script
|
||||
# Converts a value using a multiplication factor: result = value × factor
|
||||
#
|
||||
# Usage:
|
||||
# python scripts/convert.py --value 26.2 --factor 1.60934
|
||||
# python scripts/convert.py --value 75 --factor 2.20462
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a value using a multiplication factor.",
|
||||
epilog="Examples:\n"
|
||||
" python scripts/convert.py --value 26.2 --factor 1.60934\n"
|
||||
" python scripts/convert.py --value 75 --factor 2.20462",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
|
||||
parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = round(args.value * args.factor, 4)
|
||||
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to define Agent Skills entirely in code using AgentInlineSkill.
|
||||
// No SKILL.md files are needed — skills, resources, and scripts are all defined programmatically.
|
||||
//
|
||||
// Three approaches are shown using a unit-converter skill:
|
||||
// 1. Static resources — inline content provided via AddResource
|
||||
// 2. Dynamic resources — computed at runtime via a factory delegate
|
||||
// 3. Code scripts — executable delegates the agent can invoke directly
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
// --- Build the code-defined skill ---
|
||||
var unitConverterSkill = new AgentInlineSkill(
|
||||
name: "unit-converter",
|
||||
description: "Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.",
|
||||
instructions: """
|
||||
Use this skill when the user asks to convert between units.
|
||||
|
||||
1. Review the conversion-table resource to find the factor for the requested conversion.
|
||||
2. Check the conversion-policy resource for rounding and formatting rules.
|
||||
3. Use the convert script, passing the value and factor from the table.
|
||||
""")
|
||||
// 1. Static Resource: conversion tables
|
||||
.AddResource(
|
||||
"conversion-table",
|
||||
"""
|
||||
# Conversion Tables
|
||||
|
||||
Formula: **result = value × factor**
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
""")
|
||||
// 2. Dynamic Resource: conversion policy (computed at runtime)
|
||||
.AddResource("conversion-policy", () =>
|
||||
{
|
||||
const int Precision = 4;
|
||||
return $"""
|
||||
# Conversion Policy
|
||||
|
||||
**Decimal places:** {Precision}
|
||||
**Format:** Always show both the original and converted values with units
|
||||
**Generated at:** {DateTime.UtcNow:O}
|
||||
""";
|
||||
})
|
||||
// 3. Code Script: convert
|
||||
.AddScript("convert", (double value, double factor) =>
|
||||
{
|
||||
double result = Math.Round(value * factor, 4);
|
||||
return JsonSerializer.Serialize(new { value, factor, result });
|
||||
});
|
||||
|
||||
// --- Skills Provider ---
|
||||
var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
|
||||
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
Console.WriteLine("Converting units with code-defined skills");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
@@ -0,0 +1,52 @@
|
||||
# Code-Defined Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to define **Agent Skills entirely in code** using `AgentInlineSkill`.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Creating skills programmatically with `AgentInlineSkill` — no SKILL.md files needed
|
||||
- **Static resources** via `AddResource` with inline content
|
||||
- **Dynamic resources** via `AddResource` with a factory delegate (computed at runtime)
|
||||
- **Code scripts** via `AddScript` with a delegate handler
|
||||
- Using the `AgentSkillsProvider` constructor with inline skills
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter (code-defined)
|
||||
|
||||
Converts between common units using multiplication factors. Defined entirely in C# code:
|
||||
|
||||
- `conversion-table` — Static resource with factor table
|
||||
- `conversion-policy` — Dynamic resource with formatting rules (generated at runtime)
|
||||
- `convert` — Script that performs `value × factor` conversion
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
Converting units with code-defined skills
|
||||
------------------------------------------------------------
|
||||
Agent: Here are your conversions:
|
||||
|
||||
1. **26.2 miles → 42.16 km** (a marathon distance)
|
||||
2. **75 kg → 165.35 lbs**
|
||||
```
|
||||
@@ -1,7 +1,24 @@
|
||||
# AgentSkills Samples
|
||||
|
||||
Samples demonstrating Agent Skills capabilities.
|
||||
Samples demonstrating Agent Skills capabilities. Each sample shows a different way to define and use skills.
|
||||
|
||||
| Sample | Description |
|
||||
|--------|-------------|
|
||||
| [Agent_Step01_BasicSkills](Agent_Step01_BasicSkills/) | Using Agent Skills with a ChatClientAgent, including progressive disclosure and skill resources |
|
||||
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
|
||||
| [Agent_Step02_CodeDefinedSkills](Agent_Step02_CodeDefinedSkills/) | Define skills entirely in C# code using `AgentInlineSkill`, with static/dynamic resources and scripts. |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### File-Based vs Code-Defined Skills
|
||||
|
||||
| Aspect | File-Based | Code-Defined |
|
||||
|--------|-----------|--------------|
|
||||
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# |
|
||||
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) |
|
||||
| Scripts | Supported via script executor delegate | `AddScript` delegates |
|
||||
| Discovery | Automatic from directory path | Explicit via constructor |
|
||||
| Dynamic content | No (static files only) | Yes (factory delegates) |
|
||||
| Reusability | Copy skill directory | Inline or shared instances |
|
||||
|
||||
For single-source scenarios, use the `AgentSkillsProvider` constructors directly. To combine multiple skill types, use the `AgentSkillsProviderBuilder`.
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Sample subprocess-based skill script runner.
|
||||
// Executes file-based skill scripts as local subprocesses.
|
||||
// This is provided for demonstration purposes only.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Executes file-based skill scripts as local subprocesses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This runner uses the script's absolute path, converts the arguments
|
||||
/// to CLI flags, and returns captured output. It is intended for
|
||||
/// demonstration purposes only.
|
||||
/// </remarks>
|
||||
internal static class SubprocessScriptRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a skill script as a local subprocess.
|
||||
/// </summary>
|
||||
public static async Task<object?> RunAsync(
|
||||
AgentFileSkill skill,
|
||||
AgentFileSkillScript script,
|
||||
AIFunctionArguments arguments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(script.FullPath))
|
||||
{
|
||||
return $"Error: Script file not found: {script.FullPath}";
|
||||
}
|
||||
|
||||
string extension = Path.GetExtension(script.FullPath);
|
||||
string? interpreter = extension switch
|
||||
{
|
||||
".py" => "python3",
|
||||
".js" => "node",
|
||||
".sh" => "bash",
|
||||
".ps1" => "pwsh",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".",
|
||||
};
|
||||
|
||||
if (interpreter is not null)
|
||||
{
|
||||
startInfo.FileName = interpreter;
|
||||
startInfo.ArgumentList.Add(script.FullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
startInfo.FileName = script.FullPath;
|
||||
}
|
||||
|
||||
if (arguments is not null)
|
||||
{
|
||||
foreach (var (key, value) in arguments)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
if (boolValue)
|
||||
{
|
||||
startInfo.ArgumentList.Add(NormalizeKey(key));
|
||||
}
|
||||
}
|
||||
else if (value is not null)
|
||||
{
|
||||
startInfo.ArgumentList.Add(NormalizeKey(key));
|
||||
startInfo.ArgumentList.Add(value.ToString()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process? process = null;
|
||||
try
|
||||
{
|
||||
process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
{
|
||||
return $"Error: Failed to start process for script '{script.Name}'.";
|
||||
}
|
||||
|
||||
Task<string> outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
Task<string> errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string output = await outputTask.ConfigureAwait(false);
|
||||
string error = await errorTask.ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
output += $"\nStderr:\n{error}";
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
output += $"\nScript exited with code {process.ExitCode}";
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(output) ? "(no output)" : output.Trim();
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Kill the process on cancellation to avoid leaving orphaned subprocesses.
|
||||
process?.Kill(entireProcessTree: true);
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error: Failed to execute script '{script.Name}': {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
process?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a parameter key to a consistent --flag format.
|
||||
/// Models may return keys with or without leading dashes (e.g., "value" vs "--value").
|
||||
/// </summary>
|
||||
private static string NormalizeKey(string key) => "--" + key.TrimStart('-');
|
||||
}
|
||||
+3
-10
@@ -5,20 +5,13 @@
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
|
||||
|
||||
AIAgent agent = new AnthropicClient(new ClientOptions { ApiKey = apiKey })
|
||||
AIAgent agent =
|
||||
new AnthropicClient(new ClientOptions { ApiKey = apiKey })
|
||||
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
var response = await agent.RunAsync("Tell me a joke about a pirate.");
|
||||
Console.WriteLine(response);
|
||||
|
||||
// Invoke the agent with streaming support.
|
||||
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
|
||||
{
|
||||
Console.WriteLine(update);
|
||||
}
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
@@ -18,9 +18,9 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
**Note**: These samples use Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
## Using Anthropic with Azure Foundry
|
||||
## Using Anthropic with Microsoft Foundry
|
||||
|
||||
To use Anthropic with Azure Foundry, you can check the sample [AgentProviders/Agent_With_Anthropic](../AgentProviders/Agent_With_Anthropic/README.md) for more details.
|
||||
To use Anthropic with Microsoft Foundry, you can check the sample [AgentProviders/Agent_With_Anthropic](../AgentProviders/Agent_With_Anthropic/README.md) for more details.
|
||||
|
||||
## Samples
|
||||
|
||||
|
||||
+14
-6
@@ -1,16 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
|
||||
// The sample stores conversation messages in an Azure AI Foundry memory store and retrieves relevant
|
||||
// The sample stores conversation messages in a Microsoft Foundry memory store and retrieves relevant
|
||||
// memories for subsequent invocations, even across new sessions.
|
||||
//
|
||||
// Note: Memory extraction in Azure AI Foundry is asynchronous and takes time. This sample demonstrates
|
||||
// Note: Memory extraction in Microsoft Foundry is asynchronous and takes time. This sample demonstrates
|
||||
// a simple polling approach to wait for memory updates to complete before querying.
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AzureAI;
|
||||
using Microsoft.Agents.AI.FoundryMemory;
|
||||
|
||||
string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
@@ -19,6 +20,9 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLO
|
||||
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
|
||||
|
||||
// Create an AIProjectClient for Foundry with Azure Identity authentication.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
DefaultAzureCredential credential = new();
|
||||
AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
|
||||
|
||||
@@ -33,11 +37,15 @@ FoundryMemoryProvider memoryProvider = new(
|
||||
memoryStoreName,
|
||||
stateInitializer: _ => new(new FoundryMemoryProviderScope("sample-user-123")));
|
||||
|
||||
AIAgent agent = await projectClient.CreateAIAgentAsync(deploymentName,
|
||||
options: new ChatClientAgentOptions()
|
||||
FoundryAgent agent = projectClient.AsAIAgent(
|
||||
new ChatClientAgentOptions()
|
||||
{
|
||||
Name = "TravelAssistantWithFoundryMemory",
|
||||
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details."
|
||||
},
|
||||
AIContextProviders = [memoryProvider]
|
||||
});
|
||||
|
||||
@@ -54,7 +62,7 @@ await memoryProvider.EnsureStoredMemoriesDeletedAsync(session);
|
||||
Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
|
||||
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
|
||||
|
||||
// Memory extraction in Azure AI Foundry is asynchronous and takes time to process.
|
||||
// Memory extraction in Microsoft Foundry is asynchronous and takes time to process.
|
||||
// WhenUpdatesCompletedAsync polls all pending updates and waits for them to complete.
|
||||
Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
|
||||
await memoryProvider.WhenUpdatesCompletedAsync();
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
# Agent with Memory Using Azure AI Foundry
|
||||
# Agent with Memory Using Microsoft Foundry
|
||||
|
||||
This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories across sessions.
|
||||
This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories across sessions.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
@@ -13,7 +13,7 @@ This sample demonstrates how to create and run an agent that uses Azure AI Found
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Azure subscription with Azure AI Foundry project
|
||||
1. Azure subscription with Microsoft Foundry project
|
||||
2. Azure OpenAI resource with a chat model deployment (e.g., gpt-4o-mini) and an embedding model deployment (e.g., text-embedding-ada-002)
|
||||
3. .NET 10.0 SDK
|
||||
4. Azure CLI logged in (`az login`)
|
||||
@@ -21,7 +21,7 @@ This sample demonstrates how to create and run an agent that uses Azure AI Found
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Azure AI Foundry project endpoint and memory store name
|
||||
# Microsoft Foundry project endpoint and memory store name
|
||||
export AZURE_AI_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
|
||||
export AZURE_AI_MEMORY_STORE_ID="my_memory_store"
|
||||
|
||||
@@ -48,10 +48,10 @@ The agent will:
|
||||
|
||||
## Key Differences from Mem0
|
||||
|
||||
| Aspect | Mem0 | Azure AI Foundry Memory |
|
||||
| Aspect | Mem0 | Microsoft Foundry Memory |
|
||||
|--------|------|------------------------|
|
||||
| Authentication | API Key | Azure Identity (DefaultAzureCredential) |
|
||||
| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
|
||||
| Memory Types | Single memory store | User Profile + Chat Summary |
|
||||
| Hosting | Mem0 cloud or self-hosted | Azure AI Foundry managed service |
|
||||
| Hosting | Mem0 cloud or self-hosted | Microsoft Foundry managed service |
|
||||
| Store Creation | N/A (automatic) | Explicit via `EnsureMemoryStoreCreatedAsync` |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Framework Retrieval Augmented Generation (RAG)
|
||||
# Agent Framework Retrieval Augmented Generation (RAG)
|
||||
|
||||
These samples show how to create an agent with the Agent Framework that uses Memory to remember previous conversations or facts from previous conversations.
|
||||
|
||||
@@ -7,7 +7,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|
||||
|[Chat History memory](./AgentWithMemory_Step01_ChatHistoryMemory/)|This sample demonstrates how to enable an agent to remember messages from previous conversations.|
|
||||
|[Memory with MemoryStore](./AgentWithMemory_Step02_MemoryUsingMem0/)|This sample demonstrates how to create and run an agent that uses the Mem0 service to extract and retrieve individual memories.|
|
||||
|[Custom Memory Implementation](../../01-get-started/04_memory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
|
||||
|[Memory with Azure AI Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories.|
|
||||
|[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.|
|
||||
|[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.|
|
||||
|
||||
> **See also**: [Memory Search with Foundry Agents](../FoundryAgents/FoundryAgents_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Azure Foundry Agents.
|
||||
> **See also**: [Memory Search with Foundry Agents](../AgentsWithFoundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
|
||||
|
||||
@@ -4,28 +4,14 @@
|
||||
|
||||
using System.ClientModel;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
AIAgent agent =
|
||||
new ResponsesClient(new ApiKeyCredential(apiKey))
|
||||
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
ChatCompletion chatCompletion = await agent.RunAsync([chatMessage]);
|
||||
Console.WriteLine(chatCompletion.Content.Last().Text);
|
||||
|
||||
// Invoke the agent with streaming support.
|
||||
AsyncCollectionResult<StreamingChatCompletionUpdate> completionUpdates = agent.RunStreamingAsync([chatMessage]);
|
||||
await foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
|
||||
{
|
||||
if (completionUpdate.ContentUpdate.Count > 0)
|
||||
{
|
||||
Console.WriteLine(completionUpdate.ContentUpdate[0].Text);
|
||||
}
|
||||
}
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
+16
-4
@@ -73,16 +73,28 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
|
||||
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
|
||||
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
|
||||
{
|
||||
// Skip non-message items (e.g. tool calls, reasoning) that lack a "role" property
|
||||
if (!element.TryGetProperty("role"u8, out var roleElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string messageId = element.GetProperty("id"u8).ToString();
|
||||
string messageRole = element.GetProperty("role"u8).ToString();
|
||||
string messageRole = roleElement.ToString();
|
||||
Console.WriteLine($" Message ID: {messageId}");
|
||||
Console.WriteLine($" Message Role: {messageRole}");
|
||||
|
||||
foreach (var content in element.GetProperty("content").EnumerateArray())
|
||||
if (element.TryGetProperty("content"u8, out var contentElement))
|
||||
{
|
||||
string messageContentText = content.GetProperty("text"u8).ToString();
|
||||
Console.WriteLine($" Message Text: {messageContentText}");
|
||||
foreach (var content in contentElement.EnumerateArray())
|
||||
{
|
||||
if (content.TryGetProperty("text"u8, out var textElement))
|
||||
{
|
||||
Console.WriteLine($" Message Text: {textElement}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ using Qdrant.Client;
|
||||
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";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
||||
var afOverviewUrl = "https://github.com/MicrosoftDocs/semantic-kernel-docs/blob/main/agent-framework/overview/agent-framework-overview.md";
|
||||
var afOverviewUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/overview/index.md";
|
||||
var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ This sample uses Qdrant for the vector store, but this can easily be swapped out
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
|
||||
- An existing Qdrant instance. You can use a managed service or run a local instance using Docker, but the sample assumes the instance is running locally.
|
||||
|
||||
**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
|
||||
**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
|
||||
+16
-8
@@ -4,11 +4,13 @@
|
||||
|
||||
using System.ClientModel;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Agents.AI.AzureAI;
|
||||
using OpenAI;
|
||||
using OpenAI.Files;
|
||||
using OpenAI.Responses;
|
||||
using OpenAI.VectorStores;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
@@ -37,14 +39,20 @@ ClientResult<VectorStore> vectorStoreCreate = await vectorStoreClient.CreateVect
|
||||
FileIds = { uploadResult.Value.Id }
|
||||
});
|
||||
|
||||
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreCreate.Value.Id)] };
|
||||
// Use the native OpenAI SDK FileSearchTool directly with the vector store ID.
|
||||
#pragma warning disable OPENAI001
|
||||
FileSearchTool fileSearchTool = new([vectorStoreCreate.Value.Id]);
|
||||
#pragma warning restore OPENAI001
|
||||
|
||||
AIAgent agent = await aiProjectClient
|
||||
.CreateAIAgentAsync(
|
||||
model: deploymentName,
|
||||
name: "AskContoso",
|
||||
instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
|
||||
tools: [fileSearchTool]);
|
||||
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
"AskContoso",
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(model: deploymentName)
|
||||
{
|
||||
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
|
||||
Tools = { fileSearchTool }
|
||||
}));
|
||||
FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.19.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc4" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.4.0" />
|
||||
<PackageReference Include="Neo4j.AgentFramework.GraphRAG" Version="0.1.0-preview.2" />
|
||||
<PackageReference Include="Neo4j.Driver" Version="5.28.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Neo4j.AgentFramework.GraphRAG;
|
||||
using Neo4j.Driver;
|
||||
|
||||
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";
|
||||
var neo4jUri = Environment.GetEnvironmentVariable("NEO4J_URI") ?? throw new InvalidOperationException("NEO4J_URI is not set.");
|
||||
var neo4jUsername = Environment.GetEnvironmentVariable("NEO4J_USERNAME") ?? "neo4j";
|
||||
var neo4jPassword = Environment.GetEnvironmentVariable("NEO4J_PASSWORD") ?? throw new InvalidOperationException("NEO4J_PASSWORD is not set.");
|
||||
var fulltextIndex = Environment.GetEnvironmentVariable("NEO4J_FULLTEXT_INDEX_NAME") ?? "search_chunks";
|
||||
|
||||
const string RetrievalQuery = """
|
||||
MATCH (node)-[:FROM_DOCUMENT]->(doc:Document)<-[:FILED]-(company:Company)
|
||||
OPTIONAL MATCH (company)-[:FACES_RISK]->(risk:RiskFactor)
|
||||
WITH node, score, company, doc, collect(DISTINCT risk.name)[0..5] AS risks
|
||||
OPTIONAL MATCH (company)-[:MENTIONS]->(product:Product)
|
||||
WITH node, score, company, doc, risks, collect(DISTINCT product.name)[0..5] AS products
|
||||
RETURN
|
||||
node.text AS text,
|
||||
score,
|
||||
company.name AS company,
|
||||
company.ticker AS ticker,
|
||||
doc.title AS title,
|
||||
risks,
|
||||
products
|
||||
ORDER BY score DESC
|
||||
""";
|
||||
|
||||
await using var driver = GraphDatabase.Driver(new Uri(neo4jUri), AuthTokens.Basic(neo4jUsername, neo4jPassword));
|
||||
await driver.VerifyConnectivityAsync();
|
||||
|
||||
await using var provider = new Neo4jContextProvider(
|
||||
driver,
|
||||
new Neo4jContextProviderOptions
|
||||
{
|
||||
IndexName = fulltextIndex,
|
||||
IndexType = IndexType.Fulltext,
|
||||
RetrievalQuery = RetrievalQuery,
|
||||
TopK = 5,
|
||||
ContextPrompt = "Use the retrieved Neo4j graph context to answer accurately and call out when context is missing."
|
||||
});
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that answers questions using Neo4j graph context."
|
||||
},
|
||||
AIContextProviders = [provider]
|
||||
});
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
foreach (var question in new[]
|
||||
{
|
||||
"What products does Microsoft offer?",
|
||||
"What risks does Apple face?",
|
||||
"Tell me about NVIDIA's AI business and risk factors."
|
||||
})
|
||||
{
|
||||
Console.WriteLine($">> {question}\n");
|
||||
Console.WriteLine(await agent.RunAsync(question, session));
|
||||
Console.WriteLine();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Framework Retrieval Augmented Generation (RAG) with Neo4j GraphRAG
|
||||
|
||||
This sample demonstrates how to create and run an agent that uses the [Neo4j GraphRAG context provider](https://github.com/neo4j-labs/neo4j-maf-provider) with Microsoft Agent Framework for .NET.
|
||||
|
||||
The sample uses a Neo4j fulltext index for retrieval and a Cypher `RetrievalQuery` to enrich results with related companies, products, and risk factors.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI endpoint and chat deployment
|
||||
- Azure CLI installed and authenticated
|
||||
- A Neo4j database with chunked documents and a fulltext index such as `search_chunks`
|
||||
|
||||
## Environment variables
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
$env:NEO4J_URI="neo4j+s://your-instance.databases.neo4j.io"
|
||||
$env:NEO4J_USERNAME="neo4j"
|
||||
$env:NEO4J_PASSWORD="your-password"
|
||||
$env:NEO4J_FULLTEXT_INDEX_NAME="search_chunks"
|
||||
```
|
||||
|
||||
## Build and run
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
dotnet run --framework net10.0 --no-build
|
||||
```
|
||||
|
||||
The sample issues a few questions against the graph-backed retrieval provider and prints the responses to the console.
|
||||
@@ -8,3 +8,4 @@ These samples show how to create an agent with the Agent Framework that uses Ret
|
||||
|[RAG with Vector Store and custom schema](./AgentWithRAG_Step02_CustomVectorStoreRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with a vector store. It also uses a custom schema for the documents stored in the vector store.|
|
||||
|[RAG with custom RAG data source](./AgentWithRAG_Step03_CustomRAGDataSource/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with a custom RAG data source.|
|
||||
|[RAG with Foundry VectorStore service](./AgentWithRAG_Step04_FoundryServiceRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with the Foundry VectorStore service.|
|
||||
|[RAG with Neo4j GraphRAG](./AgentWithRAG_Step05_Neo4jGraphRAG/)|This sample demonstrates how to create and run an agent that uses a Neo4j-backed GraphRAG context provider with graph-enriched retrieval.|
|
||||
|
||||
@@ -18,7 +18,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource
|
||||
|
||||
**Note**: This sample uses Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
**Note**: This sample uses Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// This sample shows how to expose an AI agent as an MCP tool.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -18,11 +19,17 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
|
||||
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Create a server side agent and expose it as an AIAgent.
|
||||
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
|
||||
model: deploymentName,
|
||||
instructions: "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
|
||||
name: "Joker",
|
||||
description: "An agent that tells jokes.");
|
||||
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
"Joker",
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(model: deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
|
||||
})
|
||||
{
|
||||
Description = "An agent that tells jokes.",
|
||||
});
|
||||
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);
|
||||
|
||||
// Convert the agent to an AIFunction and then to an MCP tool.
|
||||
// The agent name and description will be used as the mcp tool name and description.
|
||||
|
||||
@@ -20,8 +20,8 @@ To use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)
|
||||
MCP Inspector is up and running at http://127.0.0.1:6274
|
||||
```
|
||||
1. Open a web browser and navigate to the URL displayed in the terminal. If not opened automatically, this will open the MCP Inspector interface.
|
||||
1. In the MCP Inspector interface, add the following environment variables to allow your MCP server to access Azure AI Foundry Project to create and run the agent:
|
||||
- AZURE_AI_PROJECT_ENDPOINT = https://your-resource.openai.azure.com/ # Replace with your Azure AI Foundry Project endpoint
|
||||
1. In the MCP Inspector interface, add the following environment variables to allow your MCP server to access Microsoft Foundry Project to create and run the agent:
|
||||
- AZURE_AI_PROJECT_ENDPOINT = https://your-resource.openai.azure.com/ # Replace with your Microsoft Foundry Project endpoint
|
||||
- AZURE_AI_MODEL_DEPLOYMENT_NAME = gpt-4o-mini # Replace with your model deployment name
|
||||
1. Find and click the `Connect` button in the MCP Inspector interface to connect to the MCP server.
|
||||
1. As soon as the connection is established, open the `Tools` tab in the MCP Inspector interface and select the `Joker` tool from the list.
|
||||
|
||||
+6
@@ -16,5 +16,11 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Assets\walkway.jpg">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@@ -22,7 +22,7 @@ var agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential(
|
||||
|
||||
ChatMessage message = new(ChatRole.User, [
|
||||
new TextContent("What do you see in this image?"),
|
||||
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
|
||||
await DataContent.LoadFromAsync("Assets/walkway.jpg"),
|
||||
]);
|
||||
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
@@ -13,7 +13,7 @@ using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
// Get Azure AI Foundry configuration from environment variables
|
||||
// Get Microsoft Foundry configuration from environment variables
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||
|
||||
@@ -189,9 +189,9 @@ async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, Agent
|
||||
// Regex patterns for PII detection (simplified for demonstration)
|
||||
Regex[] piiPatterns =
|
||||
[
|
||||
new(@"\b\d{3}-\d{3}-\d{4}\b", RegexOptions.Compiled), // Phone number (e.g., 123-456-7890)
|
||||
new(@"\b[\w\.-]+@[\w\.-]+\.\w+\b", RegexOptions.Compiled), // Email address
|
||||
new(@"\b[A-Z][a-z]+\s[A-Z][a-z]+\b", RegexOptions.Compiled) // Full name (e.g., John Doe)
|
||||
MyRegex(), // Phone number (e.g., 123-456-7890)
|
||||
EmailRegex(), // Email address
|
||||
FullNameRegex() // Full name (e.g., John Doe)
|
||||
];
|
||||
|
||||
foreach (var pattern in piiPatterns)
|
||||
@@ -309,3 +309,15 @@ internal sealed class DateTimeContextProvider : MessageAIContextProvider
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class Program
|
||||
{
|
||||
[GeneratedRegex(@"\b\d{3}-\d{3}-\d{4}\b", RegexOptions.Compiled)]
|
||||
private static partial Regex MyRegex();
|
||||
|
||||
[GeneratedRegex(@"\b[\w\.-]+@[\w\.-]+\.\w+\b", RegexOptions.Compiled)]
|
||||
private static partial Regex EmailRegex();
|
||||
|
||||
[GeneratedRegex(@"\b[A-Z][a-z]+\s[A-Z][a-z]+\b", RegexOptions.Compiled)]
|
||||
private static partial Regex FullNameRegex();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// This sample shows how to use a chat history reducer to keep the context within model size limits.
|
||||
// Any implementation of Microsoft.Extensions.AI.IChatReducer can be used to customize how the chat history is reduced.
|
||||
// NOTE: this feature is only supported where the chat history is stored locally, such as with OpenAI Chat Completion.
|
||||
// Where the chat history is stored server side, such as with Azure Foundry Agents, the service must manage the chat history size.
|
||||
// Where the chat history is stored server side, such as with Microsoft Foundry Agents, the service must manage the chat history size.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete - sample uses deprecated PersistentAgentsClientExtensions
|
||||
|
||||
// This sample shows how to create an Azure AI Foundry Agent with the Deep Research Tool.
|
||||
// This sample shows how to create a Microsoft Foundry Agent with the Deep Research Tool.
|
||||
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
@@ -17,10 +17,10 @@ var bingConnectionId = Environment.GetEnvironmentVariable("AZURE_AI_BING_CONNECT
|
||||
PersistentAgentsAdministrationClientOptions persistentAgentsClientOptions = new();
|
||||
persistentAgentsClientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(20);
|
||||
|
||||
// Get a client to create/retrieve server side agents with.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
// Get a client to create/retrieve server side agents with.
|
||||
PersistentAgentsClient persistentAgentsClient = new(endpoint, new DefaultAzureCredential(), persistentAgentsClientOptions);
|
||||
|
||||
// Define and configure the Deep Research tool.
|
||||
|
||||
@@ -11,10 +11,10 @@ Key features:
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. An Azure AI Foundry project set up
|
||||
1. A Microsoft Foundry project set up
|
||||
2. A deep research model deployment (e.g., o3-deep-research)
|
||||
3. A model deployment (e.g., gpt-4o)
|
||||
4. A Bing Connection configured in your Azure AI Foundry project
|
||||
4. A Bing Connection configured in your Microsoft Foundry project
|
||||
5. Azure CLI installed and authenticated
|
||||
|
||||
**Important**: Please visit the following documentation for detailed setup instructions:
|
||||
@@ -23,22 +23,24 @@ Before running this sample, ensure you have:
|
||||
|
||||
Pay special attention to the purple `Note` boxes in the Azure documentation.
|
||||
|
||||
**Note**: The Bing Connection ID must be from the **project**, not the resource. It has the following format:
|
||||
**Note**: The Bing Grounding Connection ID must be the **full ARM resource URI** from the project, not just the connection name. It has the following format:
|
||||
|
||||
```
|
||||
/subscriptions/<sub_id>/resourceGroups/<rg_name>/providers/<provider_name>/accounts/<account_name>/projects/<project_name>/connections/<connection_name>
|
||||
/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>/connections/<connection-name>
|
||||
```
|
||||
|
||||
You can find this in the Microsoft Foundry portal under **Management > Connected resources**, or retrieve it programmatically via the connections API (`.id` property).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
# Replace with your Azure AI Foundry project endpoint
|
||||
# Replace with your Microsoft Foundry project endpoint
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/"
|
||||
|
||||
# Replace with your Bing connection ID from the project
|
||||
$env:AZURE_AI_BING_CONNECTION_ID="/subscriptions/.../connections/your-bing-connection"
|
||||
# Replace with your Bing Grounding connection ID (full ARM resource URI)
|
||||
$env:AZURE_AI_BING_CONNECTION_ID="/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>/connections/<connection-name>"
|
||||
|
||||
# Optional, defaults to o3-deep-research
|
||||
$env:AZURE_AI_REASONING_DEPLOYMENT_NAME="o3-deep-research"
|
||||
|
||||
@@ -24,12 +24,12 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
Func<Task<string[]>> loadNextThreeCalendarEvents = async () =>
|
||||
{
|
||||
// In a real implementation, this method would connect to a calendar service
|
||||
return new string[]
|
||||
{
|
||||
return
|
||||
[
|
||||
"Doctor's appointment today at 15:00",
|
||||
"Team meeting today at 17:00",
|
||||
"Birthday party today at 20:00"
|
||||
};
|
||||
];
|
||||
};
|
||||
|
||||
// Create an agent with an AI context provider attached that aggregates two other providers:
|
||||
@@ -87,7 +87,7 @@ namespace SampleApp
|
||||
internal sealed class TodoListAIContextProvider : AIContextProvider
|
||||
{
|
||||
private static List<string> GetTodoItems(AgentSession? session)
|
||||
=> session?.StateBag.GetValue<List<string>>(nameof(TodoListAIContextProvider)) ?? new List<string>();
|
||||
=> session?.StateBag.GetValue<List<string>>(nameof(TodoListAIContextProvider)) ?? [];
|
||||
|
||||
private static void SetTodoItems(AgentSession? session, List<string> items)
|
||||
=> session?.StateBag.SetValue(nameof(TodoListAIContextProvider), items);
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how the ChatClientAgent persists chat history after each individual
|
||||
// call to the AI service, using the RequirePerServiceCallChatHistoryPersistence option.
|
||||
// When an agent uses tools, FunctionInvokingChatClient may loop multiple times
|
||||
// (service call → tool execution → service call), and intermediate messages (tool calls and
|
||||
// results) are persisted after each service call. This allows you to inspect or recover them
|
||||
// even if the process is interrupted mid-loop, but may also result in chat history that is not
|
||||
// yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases.
|
||||
//
|
||||
// To use end-of-run persistence instead (atomic run semantics), remove the
|
||||
// RequirePerServiceCallChatHistoryPersistence = true setting (or set it to false). End-of-run
|
||||
// persistence is the default behavior.
|
||||
//
|
||||
// The sample runs two multi-turn conversations: one using non-streaming (RunAsync) and one
|
||||
// using streaming (RunStreamingAsync), to demonstrate correct behavior in both modes.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
var store = Environment.GetEnvironmentVariable("AZURE_OPENAI_RESPONSES_STORE") ?? "false";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient openAIClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Define multiple tools so the model makes several tool calls in a single run.
|
||||
[Description("Get the current weather for a city.")]
|
||||
static string GetWeather([Description("The city name.")] string city) =>
|
||||
city.ToUpperInvariant() switch
|
||||
{
|
||||
"SEATTLE" => "Seattle: 55°F, cloudy with light rain.",
|
||||
"NEW YORK" => "New York: 72°F, sunny and warm.",
|
||||
"LONDON" => "London: 48°F, overcast with fog.",
|
||||
"DUBLIN" => "Dublin: 43°F, overcast with fog.",
|
||||
_ => $"{city}: weather data not available."
|
||||
};
|
||||
|
||||
[Description("Get the current time in a city.")]
|
||||
static string GetTime([Description("The city name.")] string city) =>
|
||||
city.ToUpperInvariant() switch
|
||||
{
|
||||
"SEATTLE" => "Seattle: 9:00 AM PST",
|
||||
"NEW YORK" => "New York: 12:00 PM EST",
|
||||
"LONDON" => "London: 5:00 PM GMT",
|
||||
"DUBLIN" => "Dublin: 5:00 PM GMT",
|
||||
_ => $"{city}: time data not available."
|
||||
};
|
||||
|
||||
// Create the agent — per-service-call persistence is enabled via RequirePerServiceCallChatHistoryPersistence.
|
||||
// The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat
|
||||
// history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory().
|
||||
IChatClient chatClient = string.Equals(store, "TRUE", StringComparison.OrdinalIgnoreCase) ?
|
||||
openAIClient.GetResponsesClient().AsIChatClient(deploymentName) :
|
||||
openAIClient.GetResponsesClient().AsIChatClientWithStoredOutputDisabled(deploymentName);
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
new ChatClientAgentOptions
|
||||
{
|
||||
Name = "WeatherAssistant",
|
||||
RequirePerServiceCallChatHistoryPersistence = true,
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.",
|
||||
Tools = [AIFunctionFactory.Create(GetWeather), AIFunctionFactory.Create(GetTime)]
|
||||
},
|
||||
});
|
||||
|
||||
await RunNonStreamingAsync();
|
||||
await RunStreamingAsync();
|
||||
|
||||
async Task RunNonStreamingAsync()
|
||||
{
|
||||
int lastChatHistorySize = 0;
|
||||
string lastConversationId = string.Empty;
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("\n=== Non-Streaming Mode ===");
|
||||
Console.ResetColor();
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// First turn — ask about multiple cities so the model calls tools.
|
||||
const string Prompt = "What's the weather and time in Seattle, New York, and London?";
|
||||
PrintUserMessage(Prompt);
|
||||
|
||||
var response = await agent.RunAsync(Prompt, session);
|
||||
PrintAgentResponse(response.Text);
|
||||
PrintChatHistory(session, "After run", ref lastChatHistorySize, ref lastConversationId);
|
||||
|
||||
// Second turn — follow-up to verify chat history is correct.
|
||||
const string FollowUp1 = "And Dublin?";
|
||||
PrintUserMessage(FollowUp1);
|
||||
|
||||
response = await agent.RunAsync(FollowUp1, session);
|
||||
PrintAgentResponse(response.Text);
|
||||
PrintChatHistory(session, "After second run", ref lastChatHistorySize, ref lastConversationId);
|
||||
|
||||
// Third turn — follow-up to verify chat history is correct.
|
||||
const string FollowUp2 = "Which city is the warmest?";
|
||||
PrintUserMessage(FollowUp2);
|
||||
|
||||
response = await agent.RunAsync(FollowUp2, session);
|
||||
PrintAgentResponse(response.Text);
|
||||
PrintChatHistory(session, "After third run", ref lastChatHistorySize, ref lastConversationId);
|
||||
}
|
||||
|
||||
async Task RunStreamingAsync()
|
||||
{
|
||||
int lastChatHistorySize = 0;
|
||||
string lastConversationId = string.Empty;
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("\n=== Streaming Mode ===");
|
||||
Console.ResetColor();
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// First turn — ask about multiple cities so the model calls tools.
|
||||
const string Prompt = "What's the weather and time in Seattle, New York, and London?";
|
||||
PrintUserMessage(Prompt);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write("\n[Agent] ");
|
||||
Console.ResetColor();
|
||||
|
||||
await foreach (var update in agent.RunStreamingAsync(Prompt, session))
|
||||
{
|
||||
Console.Write(update);
|
||||
|
||||
// During streaming we should be able to see updates to the chat history
|
||||
// before the full run completes, as each service call is made and persisted.
|
||||
PrintChatHistory(session, "During run", ref lastChatHistorySize, ref lastConversationId);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
PrintChatHistory(session, "After run", ref lastChatHistorySize, ref lastConversationId);
|
||||
|
||||
// Second turn — follow-up to verify chat history is correct.
|
||||
const string FollowUp1 = "And Dublin?";
|
||||
PrintUserMessage(FollowUp1);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write("\n[Agent] ");
|
||||
Console.ResetColor();
|
||||
|
||||
await foreach (var update in agent.RunStreamingAsync(FollowUp1, session))
|
||||
{
|
||||
Console.Write(update);
|
||||
|
||||
// During streaming we should be able to see updates to the chat history
|
||||
// before the full run completes, as each service call is made and persisted.
|
||||
PrintChatHistory(session, "During second run", ref lastChatHistorySize, ref lastConversationId);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
PrintChatHistory(session, "After second run", ref lastChatHistorySize, ref lastConversationId);
|
||||
|
||||
// Third turn — follow-up to verify chat history is correct.
|
||||
const string FollowUp2 = "Which city is the warmest?";
|
||||
PrintUserMessage(FollowUp2);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write("\n[Agent] ");
|
||||
Console.ResetColor();
|
||||
|
||||
await foreach (var update in agent.RunStreamingAsync(FollowUp2, session))
|
||||
{
|
||||
Console.Write(update);
|
||||
|
||||
// During streaming we should be able to see updates to the chat history
|
||||
// before the full run completes, as each service call is made and persisted.
|
||||
PrintChatHistory(session, "During third run", ref lastChatHistorySize, ref lastConversationId);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
PrintChatHistory(session, "After third run", ref lastChatHistorySize, ref lastConversationId);
|
||||
}
|
||||
|
||||
void PrintUserMessage(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write("\n[User] ");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
|
||||
void PrintAgentResponse(string? text)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write("\n[Agent] ");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine(text);
|
||||
}
|
||||
|
||||
// Helper to print the current chat history from the session.
|
||||
void PrintChatHistory(AgentSession session, string label, ref int lastChatHistorySize, ref string lastConversationId)
|
||||
{
|
||||
if (session.TryGetInMemoryChatHistory(out var history) && history.Count != lastChatHistorySize)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"\n [{label} — Chat history: {history.Count} message(s)]");
|
||||
foreach (var msg in history)
|
||||
{
|
||||
var preview = msg.Text?.Length > 80 ? msg.Text[..80] + "…" : msg.Text;
|
||||
var contentTypes = string.Join(", ", msg.Contents.Select(c => c.GetType().Name));
|
||||
Console.WriteLine($" {msg.Role,-12} | {(string.IsNullOrWhiteSpace(preview) ? $"[{contentTypes}]" : preview)}");
|
||||
}
|
||||
|
||||
Console.ResetColor();
|
||||
|
||||
lastChatHistorySize = history.Count;
|
||||
}
|
||||
|
||||
if (session is ChatClientAgentSession ccaSession && ccaSession.ConversationId is not null && ccaSession.ConversationId != lastConversationId)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{label} — Conversation ID: {ccaSession.ConversationId}]");
|
||||
Console.ResetColor();
|
||||
lastConversationId = ccaSession.ConversationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# In-Function-Loop Checkpointing
|
||||
|
||||
This sample demonstrates how `ChatClientAgent` can persist chat history after each individual call to the AI service using the `RequirePerServiceCallChatHistoryPersistence` option. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
|
||||
|
||||
## What This Sample Shows
|
||||
|
||||
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By enabling `RequirePerServiceCallChatHistoryPersistence = true`, chat history is persisted after each service call via the `PerServiceCallChatHistoryPersistingChatClient` decorator:
|
||||
|
||||
- A `PerServiceCallChatHistoryPersistingChatClient` decorator is inserted into the chat client pipeline
|
||||
- Before each service call, the decorator loads history from the `ChatHistoryProvider` and prepends it to the request
|
||||
- After each service call, the decorator notifies the `ChatHistoryProvider` (and any `AIContextProvider` instances) with the new messages
|
||||
- Only **new** messages are sent to providers on each notification — messages that were already persisted in an earlier call within the same run are deduplicated automatically
|
||||
|
||||
By default (without `RequirePerServiceCallChatHistoryPersistence`), chat history is persisted at the end of the full agent run instead. To use per-service-call persistence, set `RequirePerServiceCallChatHistoryPersistence = true` on `ChatClientAgentOptions`.
|
||||
|
||||
With `RequirePerServiceCallChatHistoryPersistence` = true, the behavior matches that of chat history stored in the underlying AI service exactly.
|
||||
|
||||
Per-service-call persistence is useful for:
|
||||
- **Crash recovery** — if the process is interrupted mid-loop, the intermediate tool calls and results are already persisted
|
||||
- **Observability** — you can inspect the chat history while the agent is still running (e.g., during streaming)
|
||||
- **Long-running tool loops** — agents with many sequential tool calls benefit from incremental persistence
|
||||
|
||||
## How It Works
|
||||
|
||||
The sample asks the agent about the weather and time in three cities. The model calls the `GetWeather` and `GetTime` tools for each city, resulting in multiple service calls within a single `RunStreamingAsync` invocation. After the run completes, the sample prints the full chat history to show all the intermediate messages that were persisted along the way.
|
||||
|
||||
### Pipeline Architecture
|
||||
|
||||
```
|
||||
ChatClientAgent
|
||||
└─ FunctionInvokingChatClient (handles tool call loop)
|
||||
└─ PerServiceCallChatHistoryPersistingChatClient (persists after each service call)
|
||||
└─ Leaf IChatClient (Azure OpenAI)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and model deployment
|
||||
- Azure CLI installed and authenticated
|
||||
|
||||
**Note**: This sample uses `DefaultAzureCredential`. Sign in with `az login` before running. For production, prefer a specific credential such as `ManagedIdentityCredential`. For more information, see the [Azure CLI authentication documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Required
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
The sample runs two conversation turns:
|
||||
|
||||
1. **First turn** — asks about weather and time in three cities. The model calls `GetWeather` and `GetTime` tools (potentially in parallel or sequentially), then provides a summary. The chat history dump after the run shows all the intermediate tool call and result messages.
|
||||
|
||||
2. **Second turn** — asks a follow-up question ("Which city is the warmest?") that uses the persisted conversation context. The chat history dump shows the full accumulated conversation.
|
||||
|
||||
The chat history printout uses `session.TryGetInMemoryChatHistory()` to inspect the in-memory storage.
|
||||
@@ -18,7 +18,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
|
||||
|
||||
**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
|
||||
|
||||
**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
@@ -45,6 +45,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[Declarative agent](./Agent_Step16_Declarative/)|This sample demonstrates how to declaratively define an agent.|
|
||||
|[Providing additional AI Context to an agent using multiple AIContextProviders](./Agent_Step17_AdditionalAIContext/)|This sample demonstrates how to inject additional AI context into a ChatClientAgent using multiple custom AIContextProvider components that are attached to the agent.|
|
||||
|[Using compaction pipeline with an agent](./Agent_Step18_CompactionPipeline/)|This sample demonstrates how to use a compaction pipeline to efficiently limit the size of the conversation history for an agent.|
|
||||
|[In-function-loop checkpointing](./Agent_Step19_InFunctionLoopCheckpointing/)|This sample demonstrates how to persist chat history after each service call during a tool-calling loop, enabling crash recovery and mid-run observability.|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create, use, and clean up a FoundryAgent backed by a server-side
|
||||
// versioned agent in Microsoft Foundry. It demonstrates the full lifecycle:
|
||||
// create agent version -> wrap as FoundryAgent -> run -> delete.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.AzureAI;
|
||||
|
||||
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-4o-mini";
|
||||
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// Create the AIProjectClient to manage server-side agents.
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential());
|
||||
|
||||
// Create a server-side agent version using the native SDK.
|
||||
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
||||
JokerName,
|
||||
new AgentVersionCreationOptions(
|
||||
new PromptAgentDefinition(model: deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes.",
|
||||
}));
|
||||
|
||||
// Wrap the agent version as a FoundryAgent using the AsAIAgent extension.
|
||||
FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
|
||||
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
// Cleanup: deletes the agent and all its versions.
|
||||
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Agent Step 00 - FoundryAgent Lifecycle
|
||||
|
||||
This sample demonstrates the full lifecycle of a `FoundryAgent` backed by a server-side versioned agent in Microsoft Foundry: create → run → delete.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Microsoft Foundry project endpoint
|
||||
- A model deployment name (defaults to `gpt-4o-mini`)
|
||||
- Azure CLI installed and authenticated
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
| --- | --- | --- |
|
||||
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | Yes |
|
||||
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name | No (defaults to `gpt-4o-mini`) |
|
||||
|
||||
## Running the sample
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/02-agents/AgentsWithFoundry
|
||||
dotnet run --project .\Agent_Step00_FoundryAgentLifecycle
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and run a basic agent with AIProjectClient.AsAIAgent(...).
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
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-4o-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent =
|
||||
new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
|
||||
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
@@ -0,0 +1,55 @@
|
||||
# Creating and Running a Basic Agent with the Responses API
|
||||
|
||||
This sample demonstrates how to create and run a basic AI agent using the `ChatClientAgent`, which uses the Microsoft Foundry Responses API directly without creating server-side agent definitions.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating a `ChatClientAgent` with instructions and a model
|
||||
- Running a simple single-turn conversation
|
||||
- No server-side agent creation or cleanup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
|
||||
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the AgentsWithFoundry sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/02-agents/AgentsWithFoundry
|
||||
dotnet run --project .\Agent_Step01_Basics
|
||||
```
|
||||
|
||||
## Alternative: Composable approach
|
||||
|
||||
You can also create the same agent by composing the underlying `IChatClient` directly. This gives you full control over the chat client pipeline:
|
||||
|
||||
```csharp
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
AIAgent agent = new ChatClientAgent(
|
||||
chatClient: aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClient().AsIChatClient(deploymentName),
|
||||
instructions: "You are good at telling jokes.",
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
This approach is useful when you need to customize the chat client pipeline or swap providers (e.g., Anthropic, OpenAI) while keeping the same agent code.
|
||||
+1
-2
@@ -9,8 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create a multi-turn conversation agent using sessions.
|
||||
// Context is preserved across multiple runs via response ID chaining in the session.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
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-4o-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
|
||||
|
||||
// Create a session to maintain context across multiple runs.
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// First turn
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
|
||||
|
||||
// Second turn — the agent remembers the first turn via the session.
|
||||
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Multi-turn Conversation
|
||||
|
||||
This sample demonstrates how to implement multi-turn conversations where context is preserved across multiple agent runs using sessions and response ID chaining.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating an agent with instructions
|
||||
- Using sessions to maintain conversation context across multiple runs
|
||||
- Response ID chaining for multi-turn conversations
|
||||
- No server-side conversation creation required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
|
||||
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the AgentsWithFoundry sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/02-agents/AgentsWithFoundry
|
||||
dotnet run --project .\Agent_Step02.1_MultiturnConversation
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use server-side conversations with a FoundryAgent.
|
||||
// Server-side conversations persist on the Foundry service and are visible in the Foundry Project UI.
|
||||
// Use this when you need conversation history to be stored and accessible server-side.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AzureAI;
|
||||
|
||||
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-4o-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
FoundryAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
|
||||
|
||||
// CreateConversationSessionAsync creates a server-side ProjectConversation
|
||||
// that persists on the Foundry service and is visible in the Foundry Project UI.
|
||||
AgentSession session = await agent.CreateConversationSessionAsync();
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
|
||||
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
|
||||
|
||||
// Streaming with server-side conversation context.
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me another joke, but about a ninja this time.", session))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user