mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c0f0ec99a | ||
|
|
6185ba2125 | ||
|
|
dcc1eeac36 | ||
|
|
3f2096595f | ||
|
|
45a9da5523 | ||
|
|
6364c05efc | ||
|
|
bbb871e4cd | ||
|
|
7d7b8dd1a4 | ||
|
|
2f51a5ca78 | ||
|
|
ad5749c92a | ||
|
|
ed6b290457 | ||
|
|
63039cb748 | ||
|
|
3e7c94699f | ||
|
|
6320443969 |
@@ -47,7 +47,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Package Versions
|
label: Package Versions
|
||||||
description: List the agent-framework-* packages and versions you are using
|
description: List the agent-framework-* packages and versions you are using
|
||||||
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-foundry: 1.0.0"
|
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ runs:
|
|||||||
|
|
||||||
- name: Test Copilot CLI
|
- name: Test Copilot CLI
|
||||||
shell: bash
|
shell: bash
|
||||||
run: copilot --version && copilot -p "What can you do in one sentence?"
|
run: copilot -p "What can you do in one sentence?"
|
||||||
|
|
||||||
- name: Azure CLI Login
|
- name: Azure CLI Login
|
||||||
uses: azure/login@v2
|
uses: azure/login@v2
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
name: Setup Local MCP Server
|
|
||||||
description: Start and validate a local streamable HTTP MCP server for integration tests
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
fallback_url:
|
|
||||||
description: Existing LOCAL_MCP_URL value to keep as a fallback if local startup fails
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
host:
|
|
||||||
description: Host interface to bind the local MCP server
|
|
||||||
required: false
|
|
||||||
default: '127.0.0.1'
|
|
||||||
port:
|
|
||||||
description: Port to bind the local MCP server
|
|
||||||
required: false
|
|
||||||
default: '8011'
|
|
||||||
mount_path:
|
|
||||||
description: Mount path for the local streamable HTTP MCP endpoint
|
|
||||||
required: false
|
|
||||||
default: '/mcp'
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
effective_url:
|
|
||||||
description: Local MCP URL when startup succeeds, otherwise the provided fallback URL
|
|
||||||
value: ${{ steps.start.outputs.effective_url }}
|
|
||||||
local_url:
|
|
||||||
description: URL of the local MCP server
|
|
||||||
value: ${{ steps.start.outputs.local_url }}
|
|
||||||
started:
|
|
||||||
description: Whether the local MCP server started and passed validation
|
|
||||||
value: ${{ steps.start.outputs.started }}
|
|
||||||
pid:
|
|
||||||
description: PID of the local MCP server process when startup succeeded
|
|
||||||
value: ${{ steps.start.outputs.pid }}
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Start and validate local MCP server
|
|
||||||
id: start
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
host="${{ inputs.host }}"
|
|
||||||
port="${{ inputs.port }}"
|
|
||||||
mount_path="${{ inputs.mount_path }}"
|
|
||||||
fallback_url="${{ inputs.fallback_url }}"
|
|
||||||
|
|
||||||
if [[ ! "$mount_path" =~ ^/ ]]; then
|
|
||||||
mount_path="/$mount_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local_url="http://${host}:${port}${mount_path}"
|
|
||||||
health_url="http://${host}:${port}/healthz"
|
|
||||||
log_file="$RUNNER_TEMP/local-mcp-server.log"
|
|
||||||
pid_file="$RUNNER_TEMP/local-mcp-server.pid"
|
|
||||||
rm -f "$log_file" "$pid_file"
|
|
||||||
|
|
||||||
server_pid="$(
|
|
||||||
python3 - "$GITHUB_WORKSPACE/python" "$log_file" "$host" "$port" "$mount_path" <<'PY'
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
workspace, log_file, host, port, mount_path = sys.argv[1:]
|
|
||||||
|
|
||||||
with open(log_file, "w", encoding="utf-8") as log:
|
|
||||||
process = subprocess.Popen(
|
|
||||||
[
|
|
||||||
"uv",
|
|
||||||
"run",
|
|
||||||
"python",
|
|
||||||
"scripts/local_mcp_streamable_http_server.py",
|
|
||||||
"--host",
|
|
||||||
host,
|
|
||||||
"--port",
|
|
||||||
port,
|
|
||||||
"--mount-path",
|
|
||||||
mount_path,
|
|
||||||
],
|
|
||||||
cwd=workspace,
|
|
||||||
stdout=log,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
start_new_session=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(process.pid)
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
echo "$server_pid" > "$pid_file"
|
|
||||||
|
|
||||||
started=false
|
|
||||||
for _ in $(seq 1 30); do
|
|
||||||
if curl --silent --fail "$health_url" >/dev/null; then
|
|
||||||
started=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$started" == "true" ]]; then
|
|
||||||
if ! (
|
|
||||||
cd "$GITHUB_WORKSPACE/python"
|
|
||||||
LOCAL_MCP_URL="$local_url" uv run python - <<'PY'
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
|
|
||||||
from agent_framework import Content, MCPStreamableHTTPTool
|
|
||||||
|
|
||||||
|
|
||||||
def result_to_text(result: str | list[Content]) -> str:
|
|
||||||
if isinstance(result, str):
|
|
||||||
return result
|
|
||||||
return "\n".join(content.text for content in result if content.type == "text" and content.text)
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
|
||||||
tool = MCPStreamableHTTPTool(
|
|
||||||
name="local_ci_mcp",
|
|
||||||
url=os.environ["LOCAL_MCP_URL"],
|
|
||||||
approval_mode="never_require",
|
|
||||||
)
|
|
||||||
|
|
||||||
async with tool:
|
|
||||||
assert tool.functions, "Local MCP server did not expose any tools."
|
|
||||||
result = result_to_text(await tool.functions[0].invoke(query="What is Agent Framework?"))
|
|
||||||
assert result, "Local MCP server returned an empty response."
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
PY
|
|
||||||
); then
|
|
||||||
started=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
effective_url="$local_url"
|
|
||||||
pid="$server_pid"
|
|
||||||
|
|
||||||
if [[ "$started" != "true" ]]; then
|
|
||||||
effective_url="$fallback_url"
|
|
||||||
pid=""
|
|
||||||
if kill -0 "$server_pid" 2>/dev/null; then
|
|
||||||
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" || true
|
|
||||||
sleep 1
|
|
||||||
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" || true
|
|
||||||
fi
|
|
||||||
echo "Local MCP server was unavailable; continuing with fallback LOCAL_MCP_URL."
|
|
||||||
if [[ -f "$log_file" ]]; then
|
|
||||||
tail -n 100 "$log_file" || true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Using local MCP server at $local_url"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "started=$started" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "local_url=$local_url" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "effective_url=$effective_url" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "pid=$pid" >> "$GITHUB_OUTPUT"
|
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
.github
|
.github
|
||||||
dotnet
|
dotnet
|
||||||
python
|
python
|
||||||
declarative-agents
|
workflow-samples
|
||||||
|
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@v5.2.0
|
uses: actions/setup-dotnet@v5.2.0
|
||||||
@@ -152,7 +152,7 @@ jobs:
|
|||||||
.github
|
.github
|
||||||
dotnet
|
dotnet
|
||||||
python
|
python
|
||||||
declarative-agents
|
workflow-samples
|
||||||
|
|
||||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||||
- name: Start Azure Cosmos DB Emulator
|
- name: Start Azure Cosmos DB Emulator
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
.github
|
.github
|
||||||
dotnet
|
dotnet
|
||||||
python
|
python
|
||||||
declarative-agents
|
workflow-samples
|
||||||
|
|
||||||
- name: Start Azure Cosmos DB Emulator
|
- name: Start Azure Cosmos DB Emulator
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ from dataclasses import dataclass
|
|||||||
# (e.g., "packages/core/agent_framework/observability.py")
|
# (e.g., "packages/core/agent_framework/observability.py")
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
ENFORCED_TARGETS: set[str] = {
|
ENFORCED_TARGETS: set[str] = {
|
||||||
# Packages (sorted alphabetically)
|
# Packages
|
||||||
"packages.anthropic.agent_framework_anthropic",
|
"packages.azure-ai.agent_framework_azure_ai",
|
||||||
"packages.azure-ai-search.agent_framework_azure_ai_search",
|
|
||||||
"packages.core.agent_framework",
|
"packages.core.agent_framework",
|
||||||
"packages.core.agent_framework._workflows",
|
"packages.core.agent_framework._workflows",
|
||||||
"packages.foundry.agent_framework_foundry",
|
|
||||||
"packages.openai.agent_framework_openai",
|
|
||||||
"packages.purview.agent_framework_purview",
|
"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",
|
||||||
# Individual files (if you want to enforce specific files instead of whole packages)
|
# Individual files (if you want to enforce specific files instead of whole packages)
|
||||||
"packages/core/agent_framework/observability.py",
|
"packages/core/agent_framework/observability.py",
|
||||||
# Add more targets here as coverage improves
|
# Add more targets here as coverage improves
|
||||||
|
|||||||
@@ -60,10 +60,9 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||||
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -82,8 +81,8 @@ jobs:
|
|||||||
- name: Test with pytest (OpenAI integration)
|
- name: Test with pytest (OpenAI integration)
|
||||||
run: >
|
run: >
|
||||||
uv run pytest --import-mode=importlib
|
uv run pytest --import-mode=importlib
|
||||||
packages/openai/tests
|
packages/core/tests/openai
|
||||||
-m "integration and not azure"
|
-m integration
|
||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
--retries 2 --retry-delay 5
|
--retries 2 --retry-delay 5
|
||||||
@@ -95,10 +94,9 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -123,9 +121,7 @@ jobs:
|
|||||||
- name: Test with pytest (Azure OpenAI integration)
|
- name: Test with pytest (Azure OpenAI integration)
|
||||||
run: >
|
run: >
|
||||||
uv run pytest --import-mode=importlib
|
uv run pytest --import-mode=importlib
|
||||||
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
|
packages/core/tests/azure
|
||||||
packages/openai/tests/openai/test_openai_chat_client_azure.py
|
|
||||||
packages/openai/tests/openai/test_openai_embedding_client_azure.py
|
|
||||||
-m integration
|
-m integration
|
||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
@@ -139,7 +135,7 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -155,13 +151,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ env.UV_PYTHON }}
|
python-version: ${{ env.UV_PYTHON }}
|
||||||
os: ${{ runner.os }}
|
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)
|
- name: Test with pytest (Anthropic, Ollama, MCP integration)
|
||||||
run: >
|
run: >
|
||||||
uv run pytest --import-mode=importlib
|
uv run pytest --import-mode=importlib
|
||||||
@@ -172,26 +161,6 @@ jobs:
|
|||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
--retries 2 --retry-delay 5
|
--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
|
# Azure Functions + Durable Task integration tests
|
||||||
python-tests-functions:
|
python-tests-functions:
|
||||||
@@ -201,17 +170,12 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
UV_PYTHON: "3.11"
|
UV_PYTHON: "3.11"
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
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_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
|
||||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
|
||||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
|
||||||
FUNCTIONS_WORKER_RUNTIME: "python"
|
FUNCTIONS_WORKER_RUNTIME: "python"
|
||||||
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
||||||
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
||||||
@@ -245,25 +209,18 @@ jobs:
|
|||||||
packages/durabletask/tests/integration_tests
|
packages/durabletask/tests/integration_tests
|
||||||
-m integration
|
-m integration
|
||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
-x
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
--timeout=360 --session-timeout=900 --timeout_method thread
|
|
||||||
--retries 2 --retry-delay 5
|
--retries 2 --retry-delay 5
|
||||||
|
|
||||||
# Foundry integration tests
|
# Azure AI integration tests
|
||||||
python-tests-foundry:
|
python-tests-azure-ai:
|
||||||
name: Python Integration Tests - Foundry
|
name: Python Integration Tests - Azure AI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
|
|
||||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
|
|
||||||
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
|
|
||||||
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
|
|
||||||
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
|
|
||||||
FOUNDRY_IMAGE_EMBEDDING_MODEL: ${{ vars.FOUNDRY_IMAGE_EMBEDDING_MODEL || '' }}
|
|
||||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -287,13 +244,7 @@ jobs:
|
|||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
run: >
|
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
|
||||||
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
|
# Azure Cosmos integration tests
|
||||||
python-tests-cosmos:
|
python-tests-cosmos:
|
||||||
@@ -350,7 +301,7 @@ jobs:
|
|||||||
python-tests-azure-openai,
|
python-tests-azure-openai,
|
||||||
python-tests-misc-integration,
|
python-tests-misc-integration,
|
||||||
python-tests-functions,
|
python-tests-functions,
|
||||||
python-tests-foundry,
|
python-tests-azure-ai,
|
||||||
python-tests-cosmos
|
python-tests-cosmos
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
azureChanged: ${{ steps.filter.outputs.azure }}
|
azureChanged: ${{ steps.filter.outputs.azure }}
|
||||||
miscChanged: ${{ steps.filter.outputs.misc }}
|
miscChanged: ${{ steps.filter.outputs.misc }}
|
||||||
functionsChanged: ${{ steps.filter.outputs.functions }}
|
functionsChanged: ${{ steps.filter.outputs.functions }}
|
||||||
foundryChanged: ${{ steps.filter.outputs.foundry }}
|
azureAiChanged: ${{ steps.filter.outputs.azure-ai }}
|
||||||
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
|
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -47,9 +47,6 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
python:
|
python:
|
||||||
- 'python/**'
|
- 'python/**'
|
||||||
- '.github/actions/setup-local-mcp-server/**'
|
|
||||||
- '.github/workflows/python-merge-tests.yml'
|
|
||||||
- '.github/workflows/python-integration-tests.yml'
|
|
||||||
core:
|
core:
|
||||||
- 'python/packages/core/agent_framework/_*.py'
|
- 'python/packages/core/agent_framework/_*.py'
|
||||||
- 'python/packages/core/agent_framework/_workflows/**'
|
- 'python/packages/core/agent_framework/_workflows/**'
|
||||||
@@ -57,28 +54,20 @@ jobs:
|
|||||||
- 'python/packages/core/agent_framework/observability.py'
|
- 'python/packages/core/agent_framework/observability.py'
|
||||||
openai:
|
openai:
|
||||||
- 'python/packages/core/agent_framework/openai/**'
|
- 'python/packages/core/agent_framework/openai/**'
|
||||||
- 'python/packages/openai/**'
|
- 'python/packages/core/tests/openai/**'
|
||||||
- 'python/samples/**/providers/openai/**'
|
|
||||||
azure:
|
azure:
|
||||||
- 'python/packages/openai/**'
|
|
||||||
- 'python/packages/core/agent_framework/azure/**'
|
- 'python/packages/core/agent_framework/azure/**'
|
||||||
- 'python/samples/**/providers/azure/**'
|
- 'python/packages/core/tests/azure/**'
|
||||||
misc:
|
misc:
|
||||||
- 'python/packages/anthropic/**'
|
- 'python/packages/anthropic/**'
|
||||||
- 'python/packages/ollama/**'
|
- 'python/packages/ollama/**'
|
||||||
- 'python/packages/core/agent_framework/_mcp.py'
|
- 'python/packages/core/agent_framework/_mcp.py'
|
||||||
- 'python/packages/core/tests/core/test_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:
|
functions:
|
||||||
- 'python/packages/azurefunctions/**'
|
- 'python/packages/azurefunctions/**'
|
||||||
- 'python/packages/durabletask/**'
|
- 'python/packages/durabletask/**'
|
||||||
foundry:
|
azure-ai:
|
||||||
- 'python/packages/foundry/**'
|
- 'python/packages/azure-ai/**'
|
||||||
- 'python/samples/**/providers/foundry/**'
|
|
||||||
- 'python/samples/02-agents/embeddings/foundry_embeddings.py'
|
|
||||||
cosmos:
|
cosmos:
|
||||||
- 'python/packages/azure-cosmos/**'
|
- 'python/packages/azure-cosmos/**'
|
||||||
# run only if 'python' files were changed
|
# run only if 'python' files were changed
|
||||||
@@ -139,10 +128,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
||||||
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -158,8 +146,8 @@ jobs:
|
|||||||
- name: Test with pytest (OpenAI integration)
|
- name: Test with pytest (OpenAI integration)
|
||||||
run: >
|
run: >
|
||||||
uv run pytest --import-mode=importlib
|
uv run pytest --import-mode=importlib
|
||||||
packages/openai/tests
|
packages/core/tests/openai
|
||||||
-m "integration and not azure"
|
-m integration
|
||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
--retries 2 --retry-delay 5
|
--retries 2 --retry-delay 5
|
||||||
@@ -192,10 +180,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
|
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -218,9 +205,7 @@ jobs:
|
|||||||
- name: Test with pytest (Azure OpenAI integration)
|
- name: Test with pytest (Azure OpenAI integration)
|
||||||
run: >
|
run: >
|
||||||
uv run pytest --import-mode=importlib
|
uv run pytest --import-mode=importlib
|
||||||
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
|
packages/core/tests/azure
|
||||||
packages/openai/tests/openai/test_openai_chat_client_azure.py
|
|
||||||
packages/openai/tests/openai/test_openai_embedding_client_azure.py
|
|
||||||
-m integration
|
-m integration
|
||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
@@ -255,7 +240,7 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -268,13 +253,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ env.UV_PYTHON }}
|
python-version: ${{ env.UV_PYTHON }}
|
||||||
os: ${{ runner.os }}
|
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)
|
- name: Test with pytest (Anthropic, Ollama, MCP integration)
|
||||||
run: >
|
run: >
|
||||||
uv run pytest --import-mode=importlib
|
uv run pytest --import-mode=importlib
|
||||||
@@ -286,26 +264,6 @@ jobs:
|
|||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
--retries 2 --retry-delay 5
|
--retries 2 --retry-delay 5
|
||||||
working-directory: ./python
|
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
|
- name: Surface failing tests
|
||||||
if: always()
|
if: always()
|
||||||
uses: pmeier/pytest-results-action@v0.7.2
|
uses: pmeier/pytest-results-action@v0.7.2
|
||||||
@@ -330,17 +288,12 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
UV_PYTHON: "3.11"
|
UV_PYTHON: "3.11"
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
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_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
|
||||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
|
||||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
|
||||||
FUNCTIONS_WORKER_RUNTIME: "python"
|
FUNCTIONS_WORKER_RUNTIME: "python"
|
||||||
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
||||||
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
AzureWebJobsStorage: "UseDevelopmentStorage=true"
|
||||||
@@ -372,8 +325,7 @@ jobs:
|
|||||||
packages/durabletask/tests/integration_tests
|
packages/durabletask/tests/integration_tests
|
||||||
-m integration
|
-m integration
|
||||||
-n logical --dist worksteal
|
-n logical --dist worksteal
|
||||||
-x
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
--timeout=360 --session-timeout=900 --timeout_method thread
|
|
||||||
--retries 2 --retry-delay 5
|
--retries 2 --retry-delay 5
|
||||||
working-directory: ./python
|
working-directory: ./python
|
||||||
- name: Surface failing tests
|
- name: Surface failing tests
|
||||||
@@ -386,22 +338,20 @@ jobs:
|
|||||||
fail-on-empty: false
|
fail-on-empty: false
|
||||||
title: Functions integration test results
|
title: Functions integration test results
|
||||||
|
|
||||||
python-tests-foundry:
|
python-tests-azure-ai:
|
||||||
name: Python Integration Tests - Foundry
|
name: Python Tests - Azure AI
|
||||||
needs: paths-filter
|
needs: paths-filter
|
||||||
if: >
|
if: >
|
||||||
github.event_name != 'pull_request' &&
|
github.event_name != 'pull_request' &&
|
||||||
needs.paths-filter.outputs.pythonChanges == 'true' &&
|
needs.paths-filter.outputs.pythonChanges == 'true' &&
|
||||||
(github.event_name != 'merge_group' ||
|
(github.event_name != 'merge_group' ||
|
||||||
needs.paths-filter.outputs.foundryChanged == 'true' ||
|
needs.paths-filter.outputs.azureAiChanged == 'true' ||
|
||||||
needs.paths-filter.outputs.coreChanged == 'true')
|
needs.paths-filter.outputs.coreChanged == 'true')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||||
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
|
|
||||||
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
|
|
||||||
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -423,13 +373,12 @@ jobs:
|
|||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
run: >
|
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
|
||||||
uv run pytest --import-mode=importlib
|
working-directory: ./python
|
||||||
packages/foundry/tests
|
- name: Test Azure AI samples
|
||||||
-m integration
|
timeout-minutes: 10
|
||||||
-n logical --dist worksteal
|
if: env.RUN_SAMPLES_TESTS == 'true'
|
||||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
run: uv run pytest tests/samples/ -m "azure-ai"
|
||||||
--retries 2 --retry-delay 5
|
|
||||||
working-directory: ./python
|
working-directory: ./python
|
||||||
- name: Surface failing tests
|
- name: Surface failing tests
|
||||||
if: always()
|
if: always()
|
||||||
@@ -511,7 +460,7 @@ jobs:
|
|||||||
python-tests-azure-openai,
|
python-tests-azure-openai,
|
||||||
python-tests-misc-integration,
|
python-tests-misc-integration,
|
||||||
python-tests-functions,
|
python-tests-functions,
|
||||||
python-tests-foundry,
|
python-tests-azure-ai,
|
||||||
python-tests-cosmos,
|
python-tests-cosmos,
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
# Required configuration for get-started samples
|
# Required configuration for get-started samples
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -41,8 +43,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||||
|
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
@@ -60,22 +64,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
# Foundry configuration
|
# Azure AI configuration
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
# Azure OpenAI configuration
|
# Azure OpenAI configuration
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
|
||||||
# OpenAI configuration
|
# OpenAI configuration
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
# GitHub MCP
|
# GitHub MCP
|
||||||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
# Observability
|
# Observability
|
||||||
ENABLE_INSTRUMENTATION: "true"
|
ENABLE_INSTRUMENTATION: "true"
|
||||||
defaults:
|
defaults:
|
||||||
@@ -94,16 +96,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_CHAT_COMPLETION_MODEL=$AZURE_OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_CHAT_MODEL=$AZURE_OPENAI_CHAT_MODEL" >> .env
|
echo "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=$AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_EMBEDDING_MODEL=$AZURE_OPENAI_EMBEDDING_MODEL" >> .env
|
|
||||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||||
echo "GITHUB_PAT=$GITHUB_PAT" >> .env
|
echo "GITHUB_PAT=$GITHUB_PAT" >> .env
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
@@ -123,9 +124,8 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -143,9 +143,8 @@ jobs:
|
|||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
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_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
@@ -158,14 +157,15 @@ jobs:
|
|||||||
name: validation-report-02-agents-openai
|
name: validation-report-02-agents-openai
|
||||||
path: python/samples/sample_validation/reports/
|
path: python/samples/sample_validation/reports/
|
||||||
|
|
||||||
validate-02-agents-azure:
|
validate-02-agents-azure-openai:
|
||||||
name: Validate 02-agents/providers/azure
|
name: Validate 02-agents/providers/azure_openai
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION || '' }}
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -182,19 +182,100 @@ jobs:
|
|||||||
|
|
||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
|
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_API_VERSION=$AZURE_OPENAI_API_VERSION" >> .env
|
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
|
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_openai --save-report --report-name 02-agents-azure-openai
|
||||||
|
|
||||||
- name: Upload validation report
|
- name: Upload validation report
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: validation-report-02-agents-azure
|
name: validation-report-02-agents-azure-openai
|
||||||
|
path: python/samples/sample_validation/reports/
|
||||||
|
|
||||||
|
validate-02-agents-azure-ai:
|
||||||
|
name: Validate 02-agents/providers/azure_ai
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: integration
|
||||||
|
env:
|
||||||
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
|
AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
|
AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
|
||||||
|
BING_CONNECTION_ID: ${{ secrets.BING_CONNECTION_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 "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
|
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME=$AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME=$AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "BING_CONNECTION_ID=$BING_CONNECTION_ID" >> .env
|
||||||
|
|
||||||
|
- name: Run sample validation
|
||||||
|
run: |
|
||||||
|
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_ai --save-report --report-name 02-agents-azure-ai
|
||||||
|
|
||||||
|
- name: Upload validation report
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: validation-report-02-agents-azure-ai
|
||||||
|
path: python/samples/sample_validation/reports/
|
||||||
|
|
||||||
|
validate-02-agents-azure-ai-agent:
|
||||||
|
name: Validate 02-agents/providers/azure_ai_agent
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: integration
|
||||||
|
env:
|
||||||
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ 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 "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
|
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
|
|
||||||
|
- name: Run sample validation
|
||||||
|
run: |
|
||||||
|
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_ai_agent --save-report --report-name 02-agents-azure-ai-agent
|
||||||
|
|
||||||
|
- name: Upload validation report
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: validation-report-02-agents-azure-ai-agent
|
||||||
path: python/samples/sample_validation/reports/
|
path: python/samples/sample_validation/reports/
|
||||||
|
|
||||||
validate-02-agents-anthropic:
|
validate-02-agents-anthropic:
|
||||||
@@ -203,7 +284,7 @@ jobs:
|
|||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -221,7 +302,7 @@ jobs:
|
|||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env
|
echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env
|
||||||
echo "ANTHROPIC_CHAT_MODEL=$ANTHROPIC_CHAT_MODEL" >> .env
|
echo "ANTHROPIC_CHAT_MODEL_ID=$ANTHROPIC_CHAT_MODEL_ID" >> .env
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
@@ -265,11 +346,11 @@ jobs:
|
|||||||
|
|
||||||
validate-02-agents-amazon:
|
validate-02-agents-amazon:
|
||||||
name: Validate 02-agents/providers/amazon
|
name: Validate 02-agents/providers/amazon
|
||||||
if: false # Temporarily disabled - requires AWS credentials
|
if: false # Temporarily disabled - requires AWS credentials
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
BEDROCK_CHAT_MODEL: ${{ vars.BEDROCK__CHATMODELID }}
|
BEDROCK_CHAT_MODEL_ID: ${{ vars.BEDROCK__CHATMODELID }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -297,7 +378,7 @@ jobs:
|
|||||||
|
|
||||||
validate-02-agents-ollama:
|
validate-02-agents-ollama:
|
||||||
name: Validate 02-agents/providers/ollama
|
name: Validate 02-agents/providers/ollama
|
||||||
if: false # Temporarily disabled - requires local Ollama server
|
if: false # Temporarily disabled - requires local Ollama server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
@@ -327,16 +408,11 @@ jobs:
|
|||||||
name: validation-report-02-agents-ollama
|
name: validation-report-02-agents-ollama
|
||||||
path: python/samples/sample_validation/reports/
|
path: python/samples/sample_validation/reports/
|
||||||
|
|
||||||
validate-02-agents-foundry:
|
validate-02-agents-foundry-local:
|
||||||
name: Validate 02-agents/providers/foundry
|
name: Validate 02-agents/providers/foundry_local
|
||||||
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
|
if: false # Temporarily disabled - requires local Foundry setup
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
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:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -351,27 +427,20 @@ jobs:
|
|||||||
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
os: ${{ runner.os }}
|
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
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
|
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry_local --save-report --report-name 02-agents-foundry-local
|
||||||
|
|
||||||
- name: Upload validation report
|
- name: Upload validation report
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: validation-report-02-agents-foundry
|
name: validation-report-02-agents-foundry-local
|
||||||
path: python/samples/sample_validation/reports/
|
path: python/samples/sample_validation/reports/
|
||||||
|
|
||||||
validate-02-agents-copilotstudio:
|
validate-02-agents-copilotstudio:
|
||||||
name: Validate 02-agents/providers/copilotstudio
|
name: Validate 02-agents/providers/copilotstudio
|
||||||
if: false # Temporarily disabled - requires Copilot Studio setup
|
if: false # Temporarily disabled - requires Copilot Studio setup
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
@@ -445,8 +514,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
# Azure AI configuration
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
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 }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -463,8 +537,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||||
|
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
@@ -479,12 +556,16 @@ jobs:
|
|||||||
|
|
||||||
validate-04-hosting:
|
validate-04-hosting:
|
||||||
name: 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
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
# Azure AI configuration
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
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 }}
|
||||||
# A2A configuration
|
# A2A configuration
|
||||||
A2A_AGENT_HOST: http://localhost:5001/
|
A2A_AGENT_HOST: http://localhost:5001/
|
||||||
defaults:
|
defaults:
|
||||||
@@ -514,22 +595,23 @@ jobs:
|
|||||||
|
|
||||||
validate-05-end-to-end:
|
validate-05-end-to-end:
|
||||||
name: 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
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
# Azure AI configuration
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
# Azure OpenAI configuration
|
# Azure OpenAI configuration
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
# Azure AI Search (for evaluation samples)
|
# Azure AI Search (for evaluation samples)
|
||||||
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
|
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
|
||||||
AZURE_SEARCH_API_KEY: ${{ secrets.AZURE_SEARCH_API_KEY }}
|
AZURE_SEARCH_API_KEY: ${{ secrets.AZURE_SEARCH_API_KEY }}
|
||||||
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
|
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
|
||||||
# Evaluation sample
|
# Evaluation sample
|
||||||
FOUNDRY_MODEL_WORKFLOW: ${{ vars.FOUNDRY_MODEL_WORKFLOW || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_AI_MODEL_DEPLOYMENT_NAME_WORKFLOW: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
FOUNDRY_MODEL_EVAL: ${{ vars.FOUNDRY_MODEL_EVAL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -560,16 +642,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
# Azure AI configuration
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
# Azure OpenAI configuration
|
# Azure OpenAI configuration
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
# OpenAI configuration
|
# OpenAI configuration
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: python
|
working-directory: python
|
||||||
@@ -586,13 +668,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
|
||||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||||
|
|
||||||
- name: Run sample validation
|
- name: Run sample validation
|
||||||
run: |
|
run: |
|
||||||
@@ -610,20 +692,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: integration
|
environment: integration
|
||||||
env:
|
env:
|
||||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
|
# Azure AI configuration
|
||||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||||
# Azure OpenAI configuration for AF
|
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
|
# Azure OpenAI configuration
|
||||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
|
||||||
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
|
||||||
# Azure OpenAI configuration for SK
|
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
|
||||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
# OpenAI configuration
|
||||||
# OpenAI key
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
|
||||||
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
|
|
||||||
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
|
|
||||||
# OpenAI configuration for SK
|
|
||||||
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
|
||||||
|
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
|
||||||
# Copilot Studio
|
# Copilot Studio
|
||||||
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
|
||||||
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
|
||||||
@@ -645,13 +724,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Create .env for samples
|
- name: Create .env for samples
|
||||||
run: |
|
run: |
|
||||||
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
|
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
|
||||||
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
|
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
|
||||||
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
|
||||||
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
|
echo "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=$AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" >> .env
|
||||||
|
echo "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=$AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" >> .env
|
||||||
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
||||||
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
|
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
|
||||||
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
|
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
|
||||||
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
|
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
|
||||||
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
|
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
|
||||||
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
|
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
|
||||||
@@ -676,12 +756,14 @@ jobs:
|
|||||||
- validate-01-get-started
|
- validate-01-get-started
|
||||||
- validate-02-agents
|
- validate-02-agents
|
||||||
- validate-02-agents-openai
|
- validate-02-agents-openai
|
||||||
- validate-02-agents-azure
|
- validate-02-agents-azure-openai
|
||||||
|
- validate-02-agents-azure-ai
|
||||||
|
- validate-02-agents-azure-ai-agent
|
||||||
- validate-02-agents-anthropic
|
- validate-02-agents-anthropic
|
||||||
- validate-02-agents-github-copilot
|
- validate-02-agents-github-copilot
|
||||||
- validate-02-agents-amazon
|
- validate-02-agents-amazon
|
||||||
- validate-02-agents-ollama
|
- validate-02-agents-ollama
|
||||||
- validate-02-agents-foundry
|
- validate-02-agents-foundry-local
|
||||||
- validate-02-agents-copilotstudio
|
- validate-02-agents-copilotstudio
|
||||||
- validate-02-agents-custom
|
- validate-02-agents-custom
|
||||||
- validate-03-workflows
|
- validate-03-workflows
|
||||||
|
|||||||
+8
-47
@@ -74,37 +74,6 @@ Contributions must maintain API signature and behavioral compatibility. Contribu
|
|||||||
that include breaking changes will be rejected. Please file an issue to discuss
|
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.
|
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
|
### Suggested Workflow
|
||||||
|
|
||||||
We use and recommend the following workflow:
|
We use and recommend the following workflow:
|
||||||
@@ -123,30 +92,22 @@ We use and recommend the following workflow:
|
|||||||
"issue-123" or "githubhandle-issue".
|
"issue-123" or "githubhandle-issue".
|
||||||
4. Make and commit your changes to your branch.
|
4. Make and commit your changes to your branch.
|
||||||
5. Add new tests corresponding to your change, if applicable.
|
5. Add new tests corresponding to your change, if applicable.
|
||||||
6. Run the relevant scripts in [the section below](#development-setup) to ensure that your build is clean and all tests are passing.
|
6. Run the relevant scripts in [the section below](#development-scripts) to ensure that your build is clean and all tests are passing.
|
||||||
7. Create a PR against the repository's **main** branch.
|
7. Create a PR against the repository's **main** branch.
|
||||||
- State in the description what issue or improvement your change is addressing.
|
- State in the description what issue or improvement your change is addressing.
|
||||||
- Verify that all the Continuous Integration checks are passing.
|
- Verify that all the Continuous Integration checks are passing.
|
||||||
8. Wait for feedback or approval of your changes from the code maintainers.
|
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.
|
9. When area owners have signed off, and all checks are green, your PR will be merged.
|
||||||
|
|
||||||
### Development Setup
|
### Development scripts
|
||||||
|
|
||||||
Each language has its own dev setup guide, coding standards, and build scripts:
|
The scripts below are used to build, test, and lint within the project.
|
||||||
|
|
||||||
- **Python**: [Dev Setup](./python/DEV_SETUP.md) · [Coding Standard](./python/CODING_STANDARD.md) · [README](./python/README.md)
|
- Python: see [python/DEV_SETUP.md](./python/DEV_SETUP.md).
|
||||||
- From the `./python` directory:
|
- .NET:
|
||||||
- Build: `uv run poe build`
|
- Build: `dotnet build`
|
||||||
- Unit tests: `uv run poe test -A -m "not integration"`
|
- Test: `dotnet test`
|
||||||
- Integration tests: `uv run poe test -A -m integration` (requires API keys/endpoints)
|
- Linting (auto-fix): `dotnet format`
|
||||||
- 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
|
### PR - CI Process
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Welcome to Microsoft Agent Framework!
|
# Welcome to Microsoft Agent Framework!
|
||||||
|
|
||||||
[](https://discord.gg/b5zjErwbQM)
|
[](https://discord.gg/b5zjErwbQM)
|
||||||
[](https://learn.microsoft.com/en-us/agent-framework/)
|
[](https://learn.microsoft.com/en-us/agent-framework/)
|
||||||
[](https://pypi.org/project/agent-framework/)
|
[](https://pypi.org/project/agent-framework/)
|
||||||
[](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
|
[](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
|
# Use `az login` to authenticate with Azure CLI
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
from agent_framework import Agent
|
from agent_framework.azure import AzureOpenAIResponsesClient
|
||||||
from agent_framework.foundry import FoundryChatClient
|
|
||||||
from azure.identity import AzureCliCredential
|
from azure.identity import AzureCliCredential
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# Initialize a chat agent with Microsoft Foundry
|
# Initialize a chat agent with Azure OpenAI Responses
|
||||||
# the endpoint, deployment name, and api version can be set via environment variables
|
# the endpoint, deployment name, and api version can be set via environment variables
|
||||||
# or they can be passed in directly to the FoundryChatClient constructor
|
# or they can be passed in directly to the AzureOpenAIResponsesClient constructor
|
||||||
agent = Agent(
|
agent = AzureOpenAIResponsesClient(
|
||||||
client=FoundryChatClient(
|
# endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
||||||
credential=AzureCliCredential(),
|
# deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
|
||||||
# project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
||||||
# model=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"],
|
# api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential
|
||||||
),
|
credential=AzureCliCredential(), # Optional, if using api_key
|
||||||
name="HaikuBot",
|
).as_agent(
|
||||||
instructions="You are an upbeat assistant that writes beautifully.",
|
name="HaikuBot",
|
||||||
|
instructions="You are an upbeat assistant that writes beautifully.",
|
||||||
)
|
)
|
||||||
|
|
||||||
print(await agent.run("Write a haiku about Microsoft Agent Framework."))
|
print(await agent.run("Write a haiku about Microsoft Agent Framework."))
|
||||||
@@ -137,21 +137,24 @@ var agent = new OpenAIClient("<apikey>")
|
|||||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
|
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
// dotnet add package Microsoft.Agents.AI.AzureAI --prerelease
|
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||||
// dotnet add package Azure.Identity
|
// dotnet add package Azure.Identity
|
||||||
// Use `az login` to authenticate with Azure CLI
|
// Use `az login` to authenticate with Azure CLI
|
||||||
using Azure.AI.Projects;
|
using System.ClientModel.Primitives;
|
||||||
using Azure.Identity;
|
using Azure.Identity;
|
||||||
using Microsoft.Agents.AI;
|
using Microsoft.Agents.AI;
|
||||||
|
using OpenAI;
|
||||||
|
using OpenAI.Responses;
|
||||||
|
|
||||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
// Replace <resource> and gpt-4o-mini with your Azure OpenAI resource name and deployment name.
|
||||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
var agent = new OpenAIClient(
|
||||||
|
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
|
||||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
|
||||||
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
.GetResponsesClient("gpt-4o-mini")
|
||||||
|
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||||
|
|
||||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||||
```
|
```
|
||||||
@@ -160,43 +163,15 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
|
|||||||
|
|
||||||
### Python
|
### Python
|
||||||
|
|
||||||
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
|
- [Getting Started with Agents](./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.)
|
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
|
||||||
- [Workflows](./python/samples/03-workflows): workflow creation and integration with agents
|
- [Getting Started with 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
|
### .NET
|
||||||
|
|
||||||
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
|
- [Getting Started with Agents](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
|
||||||
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
|
- [Agent Provider Samples](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
|
||||||
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
|
- [Workflow Samples](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
|
||||||
- [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
|
## Contributor Resources
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Declarative Agents
|
# Declarative Agents
|
||||||
|
|
||||||
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../../python/samples/02-agents/declarative/).
|
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/02-agents/declarative/).
|
||||||
+2
-2
@@ -3,13 +3,13 @@ name: MicrosoftLearnAgent
|
|||||||
description: Microsoft Learn Agent
|
description: Microsoft Learn Agent
|
||||||
instructions: You answer questions by searching the Microsoft Learn content only.
|
instructions: You answer questions by searching the Microsoft Learn content only.
|
||||||
model:
|
model:
|
||||||
id: =Env.FOUNDRY_MODEL
|
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
|
||||||
options:
|
options:
|
||||||
temperature: 0.9
|
temperature: 0.9
|
||||||
topP: 0.95
|
topP: 0.95
|
||||||
connection:
|
connection:
|
||||||
kind: remote
|
kind: remote
|
||||||
endpoint: =Env.FOUNDRY_PROJECT_ENDPOINT
|
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
|
||||||
tools:
|
tools:
|
||||||
- kind: mcp
|
- kind: mcp
|
||||||
name: microsoft_learn
|
name: microsoft_learn
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
---
|
|
||||||
status: accepted
|
|
||||||
contact: rogerbarreto
|
|
||||||
date: 2026-03-06
|
|
||||||
deciders: rogerbarreto, alliscode
|
|
||||||
consulted: ""
|
|
||||||
informed: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
# Foundry agent surface stays centered on `ChatClientAgent`
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The Microsoft Foundry integration exposes two distinct usage patterns:
|
|
||||||
|
|
||||||
1. Direct Responses usage, where callers provide model, instructions, and tools at runtime.
|
|
||||||
2. Server-side versioned agents, where callers create and manage `AgentVersion` resources through `AIProjectClient.Agents`.
|
|
||||||
|
|
||||||
We briefly explored adding public wrapper types such as `FoundryAgent`, `FoundryVersionedAgent`, and `FoundryResponsesChatClient` to make those paths feel more specialized. That direction created extra public types, duplicated existing `ChatClientAgent` behavior, and pushed samples toward compatibility helpers instead of the native Azure SDK flow.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
Keep the public surface centered on `ChatClientAgent`.
|
|
||||||
|
|
||||||
- Direct Responses scenarios use `AIProjectClient.AsAIAgent(...)`.
|
|
||||||
- Server-side versioned scenarios use native `AIProjectClient.Agents` APIs to create or retrieve agent resources, then wrap `AgentRecord` or `AgentVersion` with `AIProjectClient.AsAIAgent(...)`.
|
|
||||||
- Compatibility helpers such as `AIProjectClient.CreateAIAgentAsync(...)` and `AIProjectClient.GetAIAgentAsync(...)` remain only as obsolete migration shims.
|
|
||||||
- Public wrapper types `FoundryAgent`, `FoundryVersionedAgent`, `FoundryResponsesChatClient`, and `FoundryResponsesChatClientAgent` are not part of the chosen direction.
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
- `ChatClientAgent` is already the framework abstraction used everywhere else.
|
|
||||||
- `AIProjectClient` is the native Azure SDK entry point for versioned agent lifecycle operations.
|
|
||||||
- A single agent abstraction avoids parallel type hierarchies for the same backend.
|
|
||||||
- Samples become clearer when they show either:
|
|
||||||
- direct Responses construction via `AIProjectClient.AsAIAgent(...)`, or
|
|
||||||
- native Foundry resource management via `AIProjectClient.Agents`.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
### Direct Responses path
|
|
||||||
|
|
||||||
Use the convenience overloads on `AIProjectClient`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
|
||||||
|
|
||||||
ChatClientAgent agent = aiProjectClient.AsAIAgent(
|
|
||||||
model: deploymentName,
|
|
||||||
instructions: "You are good at telling jokes.",
|
|
||||||
name: "JokerAgent");
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use composed `ChatClientAgent`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
ProjectResponsesClient projectResponsesClient = new(new Uri(endpoint), new DefaultAzureCredential(), new AgentReference($"model:{deploymentName}"));
|
|
||||||
|
|
||||||
ChatClientAgent agent = new(
|
|
||||||
chatClient: projectResponsesClient.AsIChatClient(),
|
|
||||||
instructions: "You are good at telling jokes.",
|
|
||||||
name: "JokerAgent");
|
|
||||||
```
|
|
||||||
|
|
||||||
This path is code-first and does not create a persistent server-side agent.
|
|
||||||
|
|
||||||
### Versioned agent path
|
|
||||||
|
|
||||||
Use the convenience overloads on `AIProjectClient`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
|
||||||
|
|
||||||
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
|
||||||
"JokerAgent",
|
|
||||||
new AgentVersionCreationOptions(
|
|
||||||
new PromptAgentDefinition(deploymentName)
|
|
||||||
{
|
|
||||||
Instructions = "You are good at telling jokes."
|
|
||||||
}));
|
|
||||||
|
|
||||||
ChatClientAgent agent = aiProjectClient.AsAIAgent(version);
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use composed `ChatClientAgent`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
|
||||||
|
|
||||||
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
|
|
||||||
"JokerAgent",
|
|
||||||
new AgentVersionCreationOptions(
|
|
||||||
new PromptAgentDefinition(deploymentName)
|
|
||||||
{
|
|
||||||
Instructions = "You are good at telling jokes."
|
|
||||||
}));
|
|
||||||
|
|
||||||
ProjectResponsesClient projectResponsesClient = aiProjectClient
|
|
||||||
.GetProjectOpenAIClient()
|
|
||||||
.GetProjectResponsesClientForAgent(new AgentReference(version.Name, version.Version));
|
|
||||||
|
|
||||||
ChatClientAgent agent = new(
|
|
||||||
chatClient: projectResponsesClient.AsIChatClient(),
|
|
||||||
name: "JokerAgent");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Samples
|
|
||||||
|
|
||||||
- `FoundryAgents/` samples show the direct Responses path with `AIProjectClient.AsAIAgent(...)`.
|
|
||||||
- `FoundryVersionedAgents/` samples should show native `AIProjectClient.Agents` create/get/delete flows plus `AsAIAgent(...)`.
|
|
||||||
|
|
||||||
### Compatibility APIs
|
|
||||||
|
|
||||||
Obsolete helper extensions remain only to ease migration of existing code. New samples and new guidance should not be written against them.
|
|
||||||
|
|
||||||
## Rejected direction
|
|
||||||
|
|
||||||
Do not introduce or preserve separate public wrapper types whose main purpose is to forward to `ChatClientAgent` while carrying Foundry-specific naming.
|
|
||||||
|
|
||||||
That approach:
|
|
||||||
|
|
||||||
- duplicates lifecycle concepts already present on `AIProjectClient`,
|
|
||||||
- fragments the public API,
|
|
||||||
- complicates samples and docs,
|
|
||||||
- and makes migration harder by encouraging wrapper-specific affordances.
|
|
||||||
+1
-1
@@ -462,7 +462,7 @@ class FoundryEvals:
|
|||||||
### Azure AI: FoundryEvals Constants
|
### Azure AI: FoundryEvals Constants
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from agent_framework.foundry import FoundryEvals
|
from agent_framework_azure_ai import FoundryEvals
|
||||||
|
|
||||||
evaluators = [FoundryEvals.RELEVANCE, FoundryEvals.TOOL_CALL_ACCURACY]
|
evaluators = [FoundryEvals.RELEVANCE, FoundryEvals.TOOL_CALL_ACCURACY]
|
||||||
```
|
```
|
||||||
@@ -1,960 +0,0 @@
|
|||||||
status: proposed
|
|
||||||
date: 2026-03-23
|
|
||||||
contact: sergeymenshykh
|
|
||||||
deciders: rbarreto, westey-m, eavanvalkenburg
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agent Skills: Multi-Source Architecture
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
The Agent Framework needs a skills system that lets agents discover and use domain-specific knowledge, reference documents, and executable scripts. Skills can originate from different sources — filesystem directories (SKILL.md files), inline C# code, or reusable class libraries — and the framework must support all three uniformly while allowing extensibility, composition, and filtering.
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
- Skills must be definable from multiple sources: filesystem, inline code, reusable classes, etc
|
|
||||||
- Common abstractions are needed so the provider and builder work uniformly regardless of skill origin
|
|
||||||
- File-based scripts must support user-defined executors, enabling custom runtimes and languages; code/class-based scripts execute in-process as C# delegates
|
|
||||||
- Skills must be filterable so consumers can include or exclude specific skills based on defined criteria
|
|
||||||
- Multiple skill sources must be composable into a single provider
|
|
||||||
- It must be possible to add custom skill sources (e.g., databases, REST APIs, package registries) by implementing a common abstraction
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Model-Facing Tools
|
|
||||||
|
|
||||||
Skills are presented to the model as up to three tools that progressively disclose skill content. The system prompt lists available skill names and descriptions; the model then calls these tools on demand:
|
|
||||||
|
|
||||||
- **`load_skill(skillName)`** — returns the full skill body (instructions, listed resources, listed scripts)
|
|
||||||
- **`read_skill_resource(skillName, resourceName)`** — reads a supplementary resource (file-based or code-defined) associated with a skill
|
|
||||||
- **`run_skill_script(skillName, scriptName, arguments?)`** — executes a script associated with a skill; only registered when at least one skill contains scripts
|
|
||||||
|
|
||||||
Each tool delegates to the corresponding method on the resolved `AgentSkill` — calling `Resource.ReadAsync()` or `Script.RunAsync()` respectively.
|
|
||||||
|
|
||||||
If skills have no scripts defined, the `run_skill_script` tool is **not advertised** to the model and instructions related to script execution are **not included** in the default skills instructions.
|
|
||||||
|
|
||||||
### Abstract Base Types
|
|
||||||
|
|
||||||
The architecture defines four abstract base types that all skill variants implement:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public abstract class AgentSkill
|
|
||||||
{
|
|
||||||
public abstract AgentSkillFrontmatter Frontmatter { get; }
|
|
||||||
public abstract string Content { get; }
|
|
||||||
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
|
|
||||||
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class AgentSkillResource
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string? Description { get; }
|
|
||||||
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class AgentSkillScript
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string? Description { get; }
|
|
||||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class AgentSkillsSource
|
|
||||||
{
|
|
||||||
public abstract Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Skill metadata is captured via `AgentSkillFrontmatter`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentSkillFrontmatter
|
|
||||||
{
|
|
||||||
public AgentSkillFrontmatter(string name, string description) { ... }
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
public string Description { get; }
|
|
||||||
public string? License { get; set; }
|
|
||||||
public string? Compatibility { get; set; }
|
|
||||||
public string? AllowedTools { get; set; }
|
|
||||||
public AdditionalPropertiesDictionary? Metadata { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The type hierarchy at a glance:
|
|
||||||
|
|
||||||
```
|
|
||||||
AgentSkill (abstract) AgentSkillsSource (abstract)
|
|
||||||
├── AgentFileSkill ├── AgentFileSkillsSource (public)
|
|
||||||
└── [Programmatic] ├── AgentInMemorySkillsSource (public)
|
|
||||||
├── AgentInlineSkill ├── AggregatingAgentSkillsSource (public)
|
|
||||||
└── AgentClassSkill (abstract) └── DelegatingAgentSkillsSource (abstract, public)
|
|
||||||
├── FilteringAgentSkillsSource (public)
|
|
||||||
AgentSkillResource (abstract) ├── CachingAgentSkillsSource (public)
|
|
||||||
├── AgentFileSkillResource └── DeduplicatingAgentSkillsSource (public)
|
|
||||||
└── AgentInlineSkillResource
|
|
||||||
AgentSkillScript (abstract)
|
|
||||||
├── AgentFileSkillScript
|
|
||||||
└── AgentInlineSkillScript
|
|
||||||
```
|
|
||||||
|
|
||||||
There are two top-level categories of skills:
|
|
||||||
|
|
||||||
1. **File-Based Skills** — discovered from `SKILL.md` files on the filesystem. Resources and scripts are files in subdirectories.
|
|
||||||
2. **Programmatic Skills** — defined in C# code. These are further divided into:
|
|
||||||
- **Inline Skills** — built at runtime via the `AgentInlineSkill` class and its fluent API. Ideal for quick, agent-specific skill definitions.
|
|
||||||
- **Class-Based Skills** — defined as reusable C# classes that subclass `AgentClassSkill`. Ideal for packaging skills as shared libraries or NuGet packages.
|
|
||||||
|
|
||||||
Both programmatic skill types use `AgentInlineSkillResource` and `AgentInlineSkillScript` for their resources and scripts. They are typically served by `AgentInMemorySkillsSource`, which accepts any `AgentSkill` and is not limited to programmatic skills.
|
|
||||||
|
|
||||||
### File-Based Skills
|
|
||||||
|
|
||||||
File-based skills are authored as `SKILL.md` files on disk. Resources and scripts are discovered from corresponding subfolders within the skill directory.
|
|
||||||
|
|
||||||
**`AgentFileSkill`** — A filesystem-based skill discovered from a directory containing a `SKILL.md` file. Parsed from YAML frontmatter; content is the raw markdown body. Resources and scripts are discovered from files in corresponding subfolders:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentFileSkill : AgentSkill
|
|
||||||
{
|
|
||||||
internal AgentFileSkill(
|
|
||||||
AgentSkillFrontmatter frontmatter, string content, string path,
|
|
||||||
IReadOnlyList<AgentSkillResource>? resources = null,
|
|
||||||
IReadOnlyList<AgentSkillScript>? scripts = null) { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`AgentFileSkillResource`** — A file-based skill resource. Reads content from a file on disk relative to the skill directory:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
internal sealed class AgentFileSkillResource : AgentSkillResource
|
|
||||||
{
|
|
||||||
public AgentFileSkillResource(string name, string fullPath) { ... }
|
|
||||||
|
|
||||||
public string FullPath { get; }
|
|
||||||
|
|
||||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`AgentFileSkillScript`** — A file-based skill script that represents a script file on disk. Delegates execution to an external `AgentFileSkillScriptRunner` callback (e.g., runs Python/shell via `Process.Start`). Throws `NotSupportedException` if no executor is configured:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public delegate Task<object?> AgentFileSkillScriptRunner(
|
|
||||||
AgentFileSkill skill, AgentFileSkillScript script,
|
|
||||||
AIFunctionArguments arguments, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
public sealed class AgentFileSkillScript : AgentSkillScript
|
|
||||||
{
|
|
||||||
private readonly AgentFileSkillScriptRunner _executor;
|
|
||||||
|
|
||||||
internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
|
|
||||||
: base(name) { ... }
|
|
||||||
|
|
||||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
|
|
||||||
{
|
|
||||||
|
|
||||||
return await _executor(fileSkill, this, arguments, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The executor can be provided at the **provider level** via `AgentSkillsProviderBuilder.UseFileScriptRunner(executor)` and optionally overridden for a **particular file skill** or for a **set of skills** at the file skill source level, giving fine-grained control over how different scripts are executed.
|
|
||||||
|
|
||||||
**`AgentFileSkillsSource`** — A skill source that discovers skills from filesystem directories containing `SKILL.md` files. Recursively scans directories (max 2 levels), validates frontmatter, and enforces path traversal and symlink security checks:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed partial class AgentFileSkillsSource : AgentSkillsSource
|
|
||||||
{
|
|
||||||
public AgentFileSkillsSource(
|
|
||||||
IEnumerable<string> skillPaths,
|
|
||||||
AgentFileSkillScriptRunner scriptRunner,
|
|
||||||
AgentFileSkillsSourceOptions? options = null,
|
|
||||||
ILoggerFactory? loggerFactory = null) { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`AgentFileSkillsSourceOptions`** — Configuration options for `AgentFileSkillsSource`. Allows customizing the allowed file extensions for resources and scripts without adding constructor parameters:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentFileSkillsSourceOptions
|
|
||||||
{
|
|
||||||
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
|
|
||||||
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example** — A file-based skill on disk and how it is added to a source:
|
|
||||||
|
|
||||||
```
|
|
||||||
skills/
|
|
||||||
└── unit-converter/
|
|
||||||
├── SKILL.md # frontmatter + instructions
|
|
||||||
├── resources/
|
|
||||||
│ └── conversion-table.csv # discovered as a resource
|
|
||||||
└── scripts/
|
|
||||||
└── convert.py # discovered as a script
|
|
||||||
```
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var source = new AgentFileSkillsSource(skillPaths: ["./skills"], scriptRunner: SubprocessScriptRunner.RunAsync);
|
|
||||||
|
|
||||||
var provider = new AgentSkillsProvider(source);
|
|
||||||
|
|
||||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
||||||
{
|
|
||||||
AIContextProviders = [provider],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Programmatic Skills
|
|
||||||
|
|
||||||
Programmatic skills are defined in C# code rather than discovered from the filesystem. There are two kinds: **inline** and **class-based**. Both use `AgentInlineSkillResource` and `AgentInlineSkillScript` for resources and scripts, and are held by a single `AgentInMemorySkillsSource`.
|
|
||||||
|
|
||||||
**`AgentInMemorySkillsSource`** — A general-purpose skill source that holds any `AgentSkill` instances in memory. Although commonly used for programmatic skills (`AgentInlineSkill` and `AgentClassSkill`), it accepts any `AgentSkill` subclass and is not restricted to code-defined skills:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentInMemorySkillsSource : AgentSkillsSource
|
|
||||||
{
|
|
||||||
public AgentInMemorySkillsSource(
|
|
||||||
IEnumerable<AgentSkill> skills,
|
|
||||||
ILoggerFactory? loggerFactory = null) { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Inline Skills
|
|
||||||
|
|
||||||
Inline skills are built at runtime via the `AgentInlineSkill` class and its fluent API. They are ideal for quick, agent-specific skill definitions where a full class hierarchy would be overkill.
|
|
||||||
|
|
||||||
**`AgentInlineSkill`** — A skill defined entirely in code. Resources can be static values or functions; scripts are always functions. Constructed with name, description, and instructions, then extended with resources and scripts:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentInlineSkill : AgentSkill
|
|
||||||
{
|
|
||||||
public AgentInlineSkill(string name, string description, string instructions, string? license = null, string? compatibility = null, ...) { ... }
|
|
||||||
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions) { ... }
|
|
||||||
|
|
||||||
public AgentInlineSkill AddResource(object value, string name, string? description = null);
|
|
||||||
public AgentInlineSkill AddResource(Delegate handler, string name, string? description = null);
|
|
||||||
public AgentInlineSkill AddScript(Delegate handler, string name, string? description = null);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`AgentInlineSkillResource`** — A skill resource that wraps a static value:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentInlineSkillResource : AgentSkillResource
|
|
||||||
{
|
|
||||||
public AgentInlineSkillResource(object value, string name, string? description = null)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.FromResult<object?>(_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`AgentInlineSkillResource`** — A skill resource backed by a delegate. The delegate is invoked via an `AIFunction` each time `ReadAsync` is called, producing a dynamic (computed) value:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentInlineSkillResource : AgentSkillResource
|
|
||||||
{
|
|
||||||
public AgentInlineSkillResource(Delegate handler, string name, string? description = null)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
_function = AIFunctionFactory.Create(handler, name: name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _function.InvokeAsync(new AIFunctionArguments() { Services = serviceProvider }, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`AgentInlineSkillScript`** — A skill script backed by a delegate via an `AIFunction`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class AgentInlineSkillScript : AgentSkillScript
|
|
||||||
{
|
|
||||||
private readonly AIFunction _function;
|
|
||||||
|
|
||||||
public AgentInlineSkillScript(Delegate handler, string name, string? description = null)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
_function = AIFunctionFactory.Create(handler, name: name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonElement? ParametersSchema => _function.JsonSchema;
|
|
||||||
|
|
||||||
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
|
|
||||||
{
|
|
||||||
return await _function.InvokeAsync(arguments, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example** — Creating an inline skill with a resource and script, then adding it to a source:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var skill = new AgentInlineSkill(
|
|
||||||
name: "unit-converter",
|
|
||||||
description: "Converts between measurement units.",
|
|
||||||
instructions: """
|
|
||||||
Use this skill to convert values between metric and imperial units.
|
|
||||||
Refer to the conversion-table resource for supported unit pairs.
|
|
||||||
Run the convert script to perform conversions.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
.AddResource("kg=2.205lb, m=3.281ft, L=0.264gal", "conversion-table", "Supported unit pairs")
|
|
||||||
.AddScript(Convert, "convert", "Converts a value between units");
|
|
||||||
|
|
||||||
var source = new AgentInMemorySkillsSource([skill]);
|
|
||||||
|
|
||||||
var provider = new AgentSkillsProvider(source);
|
|
||||||
|
|
||||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
||||||
{
|
|
||||||
AIContextProviders = [provider],
|
|
||||||
});
|
|
||||||
|
|
||||||
static string Convert(double value, double factor)
|
|
||||||
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Class-Based Skills
|
|
||||||
|
|
||||||
Class-based skills are designed for packaging skills as reusable libraries. Users subclass `AgentClassSkill` and override properties. Unlike inline skills, class-based skills are self-contained, can live in shared libraries or NuGet packages, and are well-suited for dependency injection.
|
|
||||||
|
|
||||||
**`AgentClassSkill`** — An abstract base class for defining skills as reusable C# classes that bundle all skill components (frontmatter, instructions, resources, scripts) together. Designed for packaging skills as distributable libraries:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public abstract class AgentClassSkill : AgentSkill
|
|
||||||
{
|
|
||||||
public abstract string Instructions { get; }
|
|
||||||
|
|
||||||
// Content is auto-synthesized from Frontmatter + Instructions + Resources + Scripts
|
|
||||||
public override string Content =>
|
|
||||||
SkillContentBuilder.BuildContent(Frontmatter.Name, Frontmatter.Description,
|
|
||||||
SkillContentBuilder.BuildBody(Instructions, Resources, Scripts));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example** — Defining a class-based skill and adding it to a source:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class UnitConverterSkill : AgentClassSkill
|
|
||||||
{
|
|
||||||
public override AgentSkillFrontmatter Frontmatter { get; } =
|
|
||||||
new("unit-converter", "Converts between measurement units.");
|
|
||||||
|
|
||||||
public override string Instructions => """
|
|
||||||
Use this skill to convert values between metric and imperial units.
|
|
||||||
Refer to the conversion-table resource for supported unit pairs.
|
|
||||||
Run the convert script to perform conversions.
|
|
||||||
""";
|
|
||||||
|
|
||||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
|
|
||||||
[
|
|
||||||
new AgentInlineSkillResource("kg=2.205lb, m=3.281ft", "conversion-table"),
|
|
||||||
];
|
|
||||||
|
|
||||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
|
|
||||||
[
|
|
||||||
new AgentInlineSkillScript(Convert, "convert"),
|
|
||||||
];
|
|
||||||
|
|
||||||
private static string Convert(double value, double factor)
|
|
||||||
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
|
|
||||||
}
|
|
||||||
|
|
||||||
var source = new AgentInMemorySkillsSource([new UnitConverterSkill()]);
|
|
||||||
|
|
||||||
var provider = new AgentSkillsProvider(source);
|
|
||||||
|
|
||||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
||||||
{
|
|
||||||
AIContextProviders = [provider],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Filtering, Caching, and Deduplication
|
|
||||||
|
|
||||||
The following subsections present alternative approaches for handling filtering, caching, and deduplication of skills across multiple sources.
|
|
||||||
|
|
||||||
### Via Composition
|
|
||||||
|
|
||||||
In this approach, the `AgentSkillsProvider` accepts a **single** `AgentSkillsSource`. Multiple sources are composed externally via an aggregate source, and cross-cutting concerns like filtering, caching, and deduplication are implemented as **source decorators** — subclasses of `DelegatingAgentSkillsSource` that intercept `GetSkillsAsync()`.
|
|
||||||
|
|
||||||
**`FilteringAgentSkillsSource`** — A decorator that applies filter logic before returning results. The decorator pattern keeps filtering orthogonal to source implementations and allows composing multiple filters:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
|
|
||||||
{
|
|
||||||
private readonly Func<AgentSkill, bool> _predicate;
|
|
||||||
|
|
||||||
public FilteringAgentSkillsSource(AgentSkillsSource innerSource, Func<AgentSkill, bool> predicate)
|
|
||||||
: base(innerSource)
|
|
||||||
{
|
|
||||||
_predicate = predicate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var skills = await this.InnerSource.GetSkillsAsync(cancellationToken);
|
|
||||||
return skills.Where(_predicate).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`CachingAgentSkillsSource`** — A decorator that caches skills after the first load, keeping the provider stateless and giving consumers control over caching granularity per source. For example, file-based skills (expensive to discover) can be cached while code-defined skills remain uncached:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class CachingAgentSkillsSource : DelegatingAgentSkillsSource
|
|
||||||
{
|
|
||||||
private IList<AgentSkill>? _cached;
|
|
||||||
|
|
||||||
public CachingAgentSkillsSource(AgentSkillsSource innerSource)
|
|
||||||
: base(innerSource)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return _cached ??= await this.InnerSource.GetSkillsAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deduplication** is similarly implemented as a decorator (`DeduplicatingAgentSkillsSource`) that deduplicates by name (case-insensitive, first-one-wins) and logs a warning for skipped duplicates.
|
|
||||||
|
|
||||||
**Example** — Combining file-based and code-defined sources with filtering and caching:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var fileSource = new CachingAgentSkillsSource(new AgentFileSkillsSource(["./skills"]));
|
|
||||||
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
|
|
||||||
|
|
||||||
var compositeSource = new FilteringAgentSkillsSource(
|
|
||||||
new AggregatingAgentSkillsSource([fileSource, codeSource]),
|
|
||||||
filter: s => s.Frontmatter.Name != "internal");
|
|
||||||
|
|
||||||
var provider = new AgentSkillsProvider(compositeSource);
|
|
||||||
|
|
||||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
||||||
{
|
|
||||||
AIContextProviders = [provider],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Clean single-responsibility: the provider serves skills, sources provide them.
|
|
||||||
- Caching, filtering, and deduplication are composable as source decorators — each concern is a separate, testable wrapper.
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- DI is less flexible: multiple `AgentSkillsSource` implementations registered in the container cannot be auto-injected into the provider. The consumer must manually compose them via an aggregate source.
|
|
||||||
- Increased public API surface: requires additional public classes (aggregate source, caching decorators, filtering decorators) that consumers need to learn and use.
|
|
||||||
|
|
||||||
### Via AgentSkillsProvider
|
|
||||||
|
|
||||||
In this approach, the `AgentSkillsProvider` accepts **`IEnumerable<AgentSkillsSource>`** and handles aggregation, filtering, caching, and deduplication internally.
|
|
||||||
|
|
||||||
The provider aggregates skills from all registered sources, deduplicates by name (case-insensitive, first-one-wins), caches the result after the first load, and optionally applies filtering via a predicate on `AgentSkillsProviderOptions`. Duplicate skill names are logged as warnings.
|
|
||||||
|
|
||||||
**Example** — Registering multiple sources directly with the provider:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Conceptual example — in practice, use AgentSkillsProviderBuilder
|
|
||||||
var fileSource = new AgentFileSkillsSource(["./skills"]);
|
|
||||||
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
|
|
||||||
|
|
||||||
var provider = new AgentSkillsProvider(
|
|
||||||
sources: [fileSource, codeSource],
|
|
||||||
options: new AgentSkillsProviderOptions
|
|
||||||
{
|
|
||||||
Filter = s => s.Frontmatter.Name != "internal",
|
|
||||||
});
|
|
||||||
|
|
||||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
||||||
{
|
|
||||||
AIContextProviders = [provider],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- DI-friendly: register multiple `AgentSkillsSource` implementations in the container, and they are all auto-injected into `AgentSkillsProvider` via `IEnumerable<AgentSkillsSource>`.
|
|
||||||
- Smaller public API surface: no need for aggregate source, caching decorators, or filtering decorator classes — these concerns are handled internally by the provider.
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- The provider takes on multiple responsibilities — aggregation, caching, deduplication, and filtering.
|
|
||||||
- Less granular caching control: caching is all-or-nothing across sources rather than per-source as with decorators.
|
|
||||||
- Less extensible: new behaviors (e.g., ordering, TTL expiration) require modifying the provider rather than adding a decorator.
|
|
||||||
|
|
||||||
### Builder Pattern
|
|
||||||
|
|
||||||
**`AgentSkillsProviderBuilder`** provides a fluent API for composing skills from multiple sources. The builder centralizes configuration — script executors, approval callbacks, prompt templates, and filtering — so consumers don't need to know the underlying source types.
|
|
||||||
|
|
||||||
The builder internally decides how to wire up the object graph: it creates the appropriate source instances, applies caching and filtering, and returns a fully configured `AgentSkillsProvider`. This keeps the setup code concise while still allowing fine-grained control when needed.
|
|
||||||
|
|
||||||
**Example** — Using the builder to combine multiple source types with configuration:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var provider = new AgentSkillsProviderBuilder()
|
|
||||||
.UseFileSkill("./skills") // file-based source
|
|
||||||
.UseInlineSkills(codeSkill) // code-defined source
|
|
||||||
.UseClassSkills(new ClassSkill()) // class-based source
|
|
||||||
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync) // script runner
|
|
||||||
.UseScriptApproval() // optional human-in-the-loop
|
|
||||||
.UsePromptTemplate(customTemplate) // optional prompt customization
|
|
||||||
.UseFilter(s => s.Frontmatter.Name != "internal") // optional skill filtering
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
|
||||||
{
|
|
||||||
AIContextProviders = [provider],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding a Custom Skill Type
|
|
||||||
|
|
||||||
The skills framework is designed for extensibility. While file-based and inline skills cover common
|
|
||||||
scenarios, you can introduce entirely new skill types by subclassing the four base classes:
|
|
||||||
|
|
||||||
| Base class | Purpose |
|
|
||||||
|-----------------------|-----------------------------------------------------|
|
|
||||||
| `AgentSkillsSource` | Discovers and loads skills from a particular origin |
|
|
||||||
| `AgentSkill` | Holds metadata, content, resources, and scripts |
|
|
||||||
| `AgentSkillResource` | Provides supplementary content to a skill |
|
|
||||||
| `AgentSkillScript` | Represents an executable action within a skill |
|
|
||||||
|
|
||||||
The example below implements a **cloud-based skill type** where skills, resources, and scripts are
|
|
||||||
all stored in and executed through a remote cloud service (e.g., Azure Blob Storage + Azure Functions).
|
|
||||||
|
|
||||||
### Step 1 — Define a custom resource
|
|
||||||
|
|
||||||
A `CloudSkillResource` reads resource content from a cloud storage endpoint instead of the local
|
|
||||||
filesystem:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// A skill resource backed by a cloud storage endpoint.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CloudSkillResource : AgentSkillResource
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public CloudSkillResource(string name, Uri blobUri, HttpClient httpClient, string? description = null)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
BlobUri = blobUri ?? throw new ArgumentNullException(nameof(blobUri));
|
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the URI of the cloud blob that holds this resource's content.
|
|
||||||
/// </summary>
|
|
||||||
public Uri BlobUri { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<object?> ReadAsync(
|
|
||||||
IServiceProvider? serviceProvider = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _httpClient.GetStringAsync(BlobUri, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2 — Define a custom script
|
|
||||||
|
|
||||||
A `CloudSkillScript` executes a script by calling a cloud function endpoint, passing arguments as
|
|
||||||
the request body:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// A skill script executed via a cloud function endpoint.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CloudSkillScript : AgentSkillScript
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public CloudSkillScript(string name, Uri functionUri, HttpClient httpClient, string? description = null)
|
|
||||||
: base(name, description)
|
|
||||||
{
|
|
||||||
FunctionUri = functionUri ?? throw new ArgumentNullException(nameof(functionUri));
|
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the URI of the cloud function that runs this script.
|
|
||||||
/// </summary>
|
|
||||||
public Uri FunctionUri { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<object?> RunAsync(
|
|
||||||
AgentSkill skill,
|
|
||||||
AIFunctionArguments arguments,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var json = JsonSerializer.Serialize(arguments);
|
|
||||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
||||||
var response = await _httpClient.PostAsync(FunctionUri, content, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3 — Define a custom skill
|
|
||||||
|
|
||||||
A `CloudSkill` bundles cloud-specific metadata (e.g., the base endpoint) with the standard skill
|
|
||||||
shape:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// An <see cref="AgentSkill"/> whose content, resources, and scripts are stored in a cloud service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CloudSkill : AgentSkill
|
|
||||||
{
|
|
||||||
public CloudSkill(
|
|
||||||
AgentSkillFrontmatter frontmatter,
|
|
||||||
string content,
|
|
||||||
Uri endpoint,
|
|
||||||
IReadOnlyList<AgentSkillResource>? resources = null,
|
|
||||||
IReadOnlyList<AgentSkillScript>? scripts = null)
|
|
||||||
{
|
|
||||||
Frontmatter = frontmatter ?? throw new ArgumentNullException(nameof(frontmatter));
|
|
||||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
|
||||||
Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
|
|
||||||
Resources = resources;
|
|
||||||
Scripts = scripts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override string Content { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the base cloud endpoint for this skill.
|
|
||||||
/// </summary>
|
|
||||||
public Uri Endpoint { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4 — Define a custom source
|
|
||||||
|
|
||||||
A `CloudSkillsSource` discovers skills from a cloud catalog API and constructs `CloudSkill`
|
|
||||||
instances with their associated resources and scripts:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// A skill source that discovers and loads skills from a cloud catalog API.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CloudSkillsSource : AgentSkillsSource
|
|
||||||
{
|
|
||||||
private readonly Uri _catalogUri;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public CloudSkillsSource(Uri catalogUri, HttpClient httpClient)
|
|
||||||
{
|
|
||||||
_catalogUri = catalogUri ?? throw new ArgumentNullException(nameof(catalogUri));
|
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
// Fetch the skill catalog from the cloud service.
|
|
||||||
var json = await _httpClient.GetStringAsync(_catalogUri, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
var catalog = JsonSerializer.Deserialize<CloudSkillCatalog>(json)!;
|
|
||||||
|
|
||||||
var skills = new List<AgentSkill>();
|
|
||||||
|
|
||||||
foreach (var entry in catalog.Skills)
|
|
||||||
{
|
|
||||||
var frontmatter = new AgentSkillFrontmatter(entry.Name, entry.Description);
|
|
||||||
|
|
||||||
// Build cloud-backed resources.
|
|
||||||
var resources = entry.Resources
|
|
||||||
.Select(r => new CloudSkillResource(r.Name, r.BlobUri, _httpClient, r.Description))
|
|
||||||
.ToList<AgentSkillResource>();
|
|
||||||
|
|
||||||
// Build cloud-backed scripts.
|
|
||||||
var scripts = entry.Scripts
|
|
||||||
.Select(s => new CloudSkillScript(s.Name, s.FunctionUri, _httpClient, s.Description))
|
|
||||||
.ToList<AgentSkillScript>();
|
|
||||||
|
|
||||||
skills.Add(new CloudSkill(frontmatter, entry.Content, entry.Endpoint, resources, scripts));
|
|
||||||
}
|
|
||||||
|
|
||||||
return skills;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5 — Register with the builder
|
|
||||||
|
|
||||||
Use `UseSource` to wire the custom source into the provider:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var httpClient = new HttpClient();
|
|
||||||
|
|
||||||
var provider = new AgentSkillsProviderBuilder()
|
|
||||||
.UseSource(new CloudSkillsSource(
|
|
||||||
new Uri("https://my-service.example.com/skills/catalog"),
|
|
||||||
httpClient))
|
|
||||||
// Mix with other source types if needed:
|
|
||||||
.UseFileSkill("/local/skills", scriptRunner)
|
|
||||||
.UseInlineSkills(someInlineSkill)
|
|
||||||
.Build();
|
|
||||||
```
|
|
||||||
|
|
||||||
The `AgentSkillsProvider` handles all skill types uniformly — any combination of file-based, inline,
|
|
||||||
class-based, and custom skills can coexist in the same provider. Custom skills automatically
|
|
||||||
participate in the model-facing tools (`load_skill`, `read_skill_resource`, `run_skill_script`),
|
|
||||||
filtering, deduplication, and caching — no additional integration work is required.
|
|
||||||
|
|
||||||
## Script Representation: `AgentSkillScript` vs `AIFunction`
|
|
||||||
|
|
||||||
Two approaches were considered for representing executable scripts within skills:
|
|
||||||
|
|
||||||
### Option A — Custom `AgentSkillScript` abstract base class (original design)
|
|
||||||
|
|
||||||
Scripts are modeled as a custom `AgentSkillScript` abstract class with `Name`, `Description`, and
|
|
||||||
`RunAsync(AgentSkill, AIFunctionArguments, CancellationToken)`. Concrete implementations:
|
|
||||||
`AgentInlineSkillScript` (wraps a delegate/`AIFunction`) and `AgentFileSkillScript` (wraps a file path + executor delegate).
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Base type
|
|
||||||
public abstract class AgentSkillScript
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string? Description { get; }
|
|
||||||
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgentSkill exposes scripts as:
|
|
||||||
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
|
|
||||||
|
|
||||||
// Inline script wraps an AIFunction internally
|
|
||||||
var script = new AgentInlineSkillScript(ConvertUnits, "convert");
|
|
||||||
|
|
||||||
// Pre-built AIFunction must be wrapped
|
|
||||||
var script = new AgentInlineSkillScript(myAIFunction);
|
|
||||||
|
|
||||||
// Class-based skill declares scripts as:
|
|
||||||
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
|
|
||||||
[
|
|
||||||
new AgentInlineSkillScript(ConvertUnits, "convert"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Provider executes scripts by passing the owning skill:
|
|
||||||
await script.RunAsync(skill, arguments, cancellationToken);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
|
|
||||||
- **Explicit skill context at execution time.** `RunAsync` receives the owning `AgentSkill`, so any script can access skill metadata or resources during execution without requiring construction-time wiring.
|
|
||||||
- **Self-contained abstraction.** A dedicated type communicates clearly that scripts are a skills-framework concept, separate from general-purpose AI functions.
|
|
||||||
- **Easier extensibility for custom script types.** Third-party implementations can subclass `AgentSkillScript` and access the owning skill in `RunAsync` without special setup.
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
|
|
||||||
- **Wrapper overhead.** `AgentInlineSkillScript` is a thin pass-through around `AIFunction` — it adds a class, a constructor, and an indirection layer for no behavioral difference.
|
|
||||||
- **Parallel abstraction.** `AgentSkillScript` and `AIFunction` serve overlapping purposes (named callable with arguments), creating two parallel hierarchies for the same concept.
|
|
||||||
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillScript` to use them as scripts, adding ceremony.
|
|
||||||
|
|
||||||
### Option B — Reuse `AIFunction` directly
|
|
||||||
|
|
||||||
Scripts are represented as `AIFunction` (from `Microsoft.Extensions.AI`). `AgentSkill.Scripts` returns
|
|
||||||
`IReadOnlyList<AIFunction>?`. `AgentInlineSkillScript` is eliminated entirely — callers use
|
|
||||||
`AIFunctionFactory.Create(delegate, name: ...)` or pass `AIFunction` instances directly.
|
|
||||||
`AgentFileSkillScript` becomes an `AIFunction` subclass that captures its owning `AgentFileSkill` via
|
|
||||||
an internal back-reference set during construction.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// AgentSkill exposes scripts as AIFunction directly:
|
|
||||||
public abstract IReadOnlyList<AIFunction>? Scripts { get; }
|
|
||||||
|
|
||||||
// Inline scripts use AIFunctionFactory — no wrapper class needed
|
|
||||||
var skill = new AgentInlineSkill("my-skill", "desc", "instructions");
|
|
||||||
skill.AddScript(ConvertUnits, "convert"); // delegate
|
|
||||||
skill.AddScript(myAIFunction); // pre-built AIFunction — no wrapping
|
|
||||||
|
|
||||||
// Class-based skill declares scripts as:
|
|
||||||
public override IReadOnlyList<AIFunction>? Scripts { get; } =
|
|
||||||
[
|
|
||||||
AIFunctionFactory.Create(ConvertUnits, name: "convert"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Provider executes scripts via standard AIFunction invocation:
|
|
||||||
await script.InvokeAsync(arguments, cancellationToken);
|
|
||||||
|
|
||||||
// File-based scripts extend AIFunction and capture the owning skill internally:
|
|
||||||
public sealed class AgentFileSkillScript : AIFunction
|
|
||||||
{
|
|
||||||
internal AgentFileSkill? Skill { get; set; } // set by AgentFileSkill constructor
|
|
||||||
|
|
||||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
|
||||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return await _executor(Skill!, this, arguments, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
|
|
||||||
- **Fewer types.** Eliminates `AgentSkillScript` and `AgentInlineSkillScript`, reducing the public API surface by two classes.
|
|
||||||
- **Seamless interop.** Any `AIFunction` — whether from `AIFunctionFactory`, a custom subclass, or an external library — can be used as a skill script with zero wrapping.
|
|
||||||
- **Consistent with `Microsoft.Extensions.AI` ecosystem.** Scripts share the same type as tool functions used by `IChatClient` and `FunctionInvokingChatClient`, reducing conceptual overhead for developers already familiar with the ecosystem.
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
|
|
||||||
- **No owning-skill context in invocation signature.** `AIFunction.InvokeAsync` does not accept an `AgentSkill` parameter, so `AgentFileSkillScript` must capture its owning skill via an internal setter during construction. This adds a construction-order dependency: the skill must set the back-reference on its scripts.
|
|
||||||
- **Custom script types lose automatic skill access.** Third-party `AIFunction` subclasses that need the owning skill must implement their own mechanism (e.g., constructor injection, closure capture) instead of receiving it as a method parameter.
|
|
||||||
- **Semantic overloading.** `AIFunction` now means both "a tool the model can call" and "a script within a skill", which could blur the distinction for framework users.
|
|
||||||
|
|
||||||
## Resource Representation: `AgentSkillResource` vs `AIFunction`
|
|
||||||
|
|
||||||
Two approaches were considered for representing skill resources (supplementary content such as references, assets, or dynamic data):
|
|
||||||
|
|
||||||
### Option A — Custom `AgentSkillResource` abstract base class (original design)
|
|
||||||
|
|
||||||
Resources are modeled as a custom `AgentSkillResource` abstract class with `Name`, `Description`, and
|
|
||||||
`ReadAsync(IServiceProvider?, CancellationToken)`. Concrete implementations:
|
|
||||||
`AgentInlineSkillResource` (static value, delegate, or `AIFunction` wrapper) and `AgentFileSkillResource` (reads file content from disk).
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Base type
|
|
||||||
public abstract class AgentSkillResource
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string? Description { get; }
|
|
||||||
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgentSkill exposes resources as:
|
|
||||||
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
|
|
||||||
|
|
||||||
// Static resource
|
|
||||||
var resource = new AgentInlineSkillResource("static content", "my-resource");
|
|
||||||
|
|
||||||
// Dynamic resource (delegate)
|
|
||||||
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
|
|
||||||
|
|
||||||
// Pre-built AIFunction must be wrapped
|
|
||||||
var resource = new AgentInlineSkillResource(myAIFunction);
|
|
||||||
|
|
||||||
// Class-based skill declares resources as:
|
|
||||||
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
|
|
||||||
[
|
|
||||||
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Provider reads resources via:
|
|
||||||
await resource.ReadAsync(serviceProvider, cancellationToken);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
|
|
||||||
- **Clear semantic distinction.** A dedicated `AgentSkillResource` type distinguishes resources (data providers) from scripts (executable actions), making the API self-documenting.
|
|
||||||
- **Purpose-built API.** `ReadAsync` communicates intent better than `InvokeAsync` for a data-access operation.
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
|
|
||||||
- **Wrapper overhead.** `AgentInlineSkillResource` wraps `AIFunction` internally for delegate/function cases — adding a class and indirection for no behavioral difference.
|
|
||||||
- **Parallel abstraction.** `AgentSkillResource` and `AIFunction` serve overlapping purposes (named callable that returns data), creating two parallel hierarchies.
|
|
||||||
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillResource`, adding ceremony.
|
|
||||||
|
|
||||||
### Option B — Reuse `AIFunction` directly
|
|
||||||
|
|
||||||
Resources are represented as `AIFunction`. `AgentSkill.Resources` returns `IReadOnlyList<AIFunction>?`.
|
|
||||||
`AgentInlineSkillResource` becomes an `AIFunction` subclass (retained as a convenience for the static-value
|
|
||||||
pattern: `new AgentInlineSkillResource("data", "name")`). `AgentFileSkillResource` becomes an `AIFunction`
|
|
||||||
subclass that reads file content.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// AgentSkill exposes resources as AIFunction directly:
|
|
||||||
public abstract IReadOnlyList<AIFunction>? Resources { get; }
|
|
||||||
|
|
||||||
// Static resource — AgentInlineSkillResource is retained as a convenience AIFunction subclass
|
|
||||||
var resource = new AgentInlineSkillResource("static content", "my-resource");
|
|
||||||
|
|
||||||
// Dynamic resource — AgentInlineSkillResource wraps delegate as AIFunction
|
|
||||||
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
|
|
||||||
|
|
||||||
// Pre-built AIFunction can be used directly — no wrapping needed
|
|
||||||
skill.AddResource(myAIFunction);
|
|
||||||
|
|
||||||
// Class-based skill declares resources as:
|
|
||||||
public override IReadOnlyList<AIFunction>? Resources { get; } =
|
|
||||||
[
|
|
||||||
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Provider reads resources via standard AIFunction invocation:
|
|
||||||
await resource.InvokeAsync(arguments, cancellationToken);
|
|
||||||
|
|
||||||
// File-based resources extend AIFunction directly:
|
|
||||||
internal sealed class AgentFileSkillResource : AIFunction
|
|
||||||
{
|
|
||||||
public string FullPath { get; }
|
|
||||||
|
|
||||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
|
||||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return await File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
|
|
||||||
- **Fewer base types.** Eliminates the `AgentSkillResource` abstract class, reducing the public API surface.
|
|
||||||
- **Seamless interop.** Any `AIFunction` can be used as a skill resource with zero wrapping.
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
|
|
||||||
- **Loss of semantic distinction.** Resources and scripts are now both `AIFunction`, which could make it less obvious which list a function belongs to when reading code.
|
|
||||||
- **Static values require a wrapper.** Unlike the original `ReadAsync` which could return a stored value directly, `AIFunction.InvokeAsync` implies invocation. `AgentInlineSkillResource` is retained as a convenience subclass to handle the static-value case, so this is not eliminated — just moved to a different class.
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
### 1. Keep `AgentSkillResource` and `AgentSkillScript` (Option A for both sections)
|
|
||||||
|
|
||||||
We are staying with the custom `AgentSkillResource` and `AgentSkillScript` model classes instead of reusing `AIFunction`:
|
|
||||||
|
|
||||||
- **Resources have no parameters.** If a consumer provides an `AIFunction` with parameters, those parameters will never be advertised to the LLM, and the resulting call will fail.
|
|
||||||
- **Approval breaks for `AIFunction`-based representations.** When a resource or script represented by an `AIFunction` is configured with approval, the second approval invocation will not work correctly.
|
|
||||||
- **Injecting the owning skill into an `AIFunction`-based script is problematic.** Constructor injection would introduce a circular reference between the skill and the script. An internal property setter is possible but adds coupling.
|
|
||||||
|
|
||||||
### 2. Make all agent skill classes internal
|
|
||||||
|
|
||||||
All agent-skill-related classes are made `internal` to minimize the public API surface while the feature matures. We can reconsider and promote types to `public` later based on community signal.
|
|
||||||
|
|
||||||
This leaves two public entry points:
|
|
||||||
|
|
||||||
- **`AgentSkillsProvider`** — use directly when all skills come from a single source and filtering is not needed.
|
|
||||||
- **`AgentSkillsProviderBuilder`** — use when mixing skill types or when filtering support is required.
|
|
||||||
|
|
||||||
### 3. Caching at provider level
|
|
||||||
|
|
||||||
Caching of tools and instructions is implemented inside `AgentSkillsProvider` rather than as an external decorator. Recreating tools and instructions on every provider call is wasteful, and a caching decorator sitting outside the provider would not have the information needed to cache them effectively.
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
---
|
|
||||||
status: accepted
|
|
||||||
contact: eavanvalkenburg
|
|
||||||
date: 2026-03-20
|
|
||||||
deciders: eavanvalkenburg, sphenry, chetantoshnival
|
|
||||||
consulted: taochenosu, moonbox3, dmytrostruk, giles17, alliscode
|
|
||||||
---
|
|
||||||
|
|
||||||
# Provider-Leading Client Design & OpenAI Package Extraction
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
The `agent-framework-core` package currently bundles OpenAI and Azure OpenAI client implementations along with their dependencies (`openai`, `azure-identity`, `azure-ai-projects`, `packaging`). This makes core heavier than necessary for users who don't use OpenAI, and it conflates the core abstractions with a specific provider implementation. Additionally, the current class naming (`OpenAIResponsesClient`, `OpenAIChatClient`) is based on the underlying OpenAI API names rather than what users actually want to do, making discoverability harder for newcomers.
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
- **Lightweight core**: Core should only contain abstractions, middleware infrastructure, and telemetry — no provider-specific code or dependencies.
|
|
||||||
- **Discoverability-first**: Import namespaces should guide users to the right client. `from agent_framework.openai import ...` should surface all OpenAI-related clients; `from agent_framework.azure import ...` should surface Foundry, Azure AI, and other Azure-specific classes.
|
|
||||||
- **Provider-leading naming**: The primary client name should reflect the provider, not the underlying API. The Responses API is now the recommended default for OpenAI, so its client should be called `OpenAIChatClient` (not `OpenAIResponsesClient`).
|
|
||||||
- **Clean separation of concerns**: Azure-specific deprecated wrappers belong in the azure-ai package, not in the OpenAI package.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
- **Keep OpenAI in core**: Simpler but keeps core heavy; doesn't help discoverability.
|
|
||||||
- **Extract OpenAI with Azure wrappers in the OpenAI package**: Keeps Azure OpenAI wrappers alongside OpenAI code, but pollutes the OpenAI package with Azure concerns.
|
|
||||||
- **Extract OpenAI, place Azure wrappers in azure-ai**: Clean separation; the OpenAI package has zero Azure dependencies; deprecated Azure wrappers live in a single file in azure-ai for easy future deletion.
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
Chosen option: "Extract OpenAI, place Azure wrappers in azure-ai", because it achieves the lightest core, cleanest OpenAI package, and the most maintainable deprecation path.
|
|
||||||
|
|
||||||
Key changes:
|
|
||||||
|
|
||||||
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
|
|
||||||
2. **Class renames**: `OpenAIResponsesClient` → `OpenAIChatClient` (Responses API), `OpenAIChatClient` → `OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
|
|
||||||
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
|
|
||||||
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
|
|
||||||
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
|
|
||||||
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
|
|
||||||
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
|
|
||||||
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
|
|
||||||
|
|
||||||
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
|
|
||||||
|
|
||||||
The existing `AzureAIClient` combines two concerns: CRUD lifecycle management (creating/deleting agents on the service) and runtime communication (sending messages via the Responses API). The new design removes CRUD entirely — users connect to agents that already exist in Foundry.
|
|
||||||
|
|
||||||
**Two approaches were considered:**
|
|
||||||
|
|
||||||
**Option A — `FoundryAgentClient` only (public ChatClient):**
|
|
||||||
Users compose `Agent(client=FoundryAgentClient(...), tools=[...])`. This follows the universal `Agent(client=X)` pattern used by every other provider. However, a "client" that wraps a named remote agent (with `agent_name` as a constructor param) is semantically odd — clients typically wrap a model endpoint, not a specific agent.
|
|
||||||
|
|
||||||
**Option B — `FoundryAgent` (Agent subclass) + private `_FoundryAgentChatClient` and public `RawFoundryAgentChatClient`:**
|
|
||||||
Users write `FoundryAgent(agent_name="my-agent", ...)` for the common case. Internally, `FoundryAgent` creates a `_FoundryAgentChatClient` and passes it to the standard `Agent` base class. For advanced customization, users pass `client_type=RawFoundryAgentChatClient` (or a custom subclass) to control the client middleware layers. The `Agent(client=RawFoundryAgentChatClient(...))` composition pattern still works for users who prefer it.
|
|
||||||
|
|
||||||
**Chosen option: Option B**, because:
|
|
||||||
- The common case (`FoundryAgent(...)`) is a single object with no boilerplate.
|
|
||||||
- `client_type=` gives full control over client middleware without parameter duplication — the agent forwards connection params to the client internally.
|
|
||||||
- `RawFoundryAgent(RawAgent)` and `FoundryAgent(Agent)` mirror the established `RawAgent`/`Agent` pattern.
|
|
||||||
- Runtime validation (only `FunctionTool` allowed) lives in `RawFoundryAgentChatClient._prepare_options`, ensuring it applies regardless of how the client is used — through `FoundryAgent`, `Agent(client=...)`, or any custom composition.
|
|
||||||
|
|
||||||
**Public classes:**
|
|
||||||
- `RawFoundryAgentChatClient(RawOpenAIChatClient)` — Responses API client that injects agent reference and validates tools. Extension point for custom client middleware.
|
|
||||||
- `RawFoundryAgent(RawAgent)` — Agent without agent-level middleware/telemetry.
|
|
||||||
- `FoundryAgent(AgentTelemetryLayer, AgentMiddlewareLayer, RawFoundryAgent)` — Recommended production agent.
|
|
||||||
|
|
||||||
**Internal (private):**
|
|
||||||
- `_FoundryAgentChatClient` — Full client with function invocation, chat middleware, and telemetry layers. Created automatically by `FoundryAgent`; users customize via `client_type=RawFoundryAgentChatClient` or a custom subclass.
|
|
||||||
|
|
||||||
**Deprecated:**
|
|
||||||
- `AzureAIClient` — replaced by `FoundryAgent` (which uses `FoundryAgentClient` internally).
|
|
||||||
- `AzureAIAgentClient` — refers to V1 Agents Service API, no direct replacement.
|
|
||||||
- `AzureAIProjectAgentProvider` — replaced by `FoundryAgent`.
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
---
|
|
||||||
status: accepted
|
|
||||||
contact: westey-m
|
|
||||||
date: 2026-03-23
|
|
||||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
|
|
||||||
consulted:
|
|
||||||
informed:
|
|
||||||
---
|
|
||||||
|
|
||||||
# Chat History Persistence Consistency
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
When using `ChatClientAgent` with tools, the `FunctionInvokingChatClient` (FIC) loops multiple times — service call → tool execution → service call → … — before producing a final response. There are two points of discrepancy between how chat history is stored by the framework's `ChatHistoryProvider` and how the underlying AI service stores chat history (e.g., OpenAI Responses with `store=true`):
|
|
||||||
|
|
||||||
1. **Persistence timing**: The AI service persists messages after *each* service call within the FIC loop. The `ChatHistoryProvider` currently persists messages only once, at the *end* of the full agent run (after all FIC loop iterations complete).
|
|
||||||
|
|
||||||
2. **Trailing `FunctionResultContent` storage**: When tool calling is terminated mid-loop (e.g., via `FunctionInvokingChatClient` termination filters), the final response from the agent may contain `FunctionResultContent` that was never sent to a subsequent service call. The AI service never stores this trailing `FunctionResultContent`, but the `ChatHistoryProvider` currently stores all response content, including the trailing `FunctionResultContent`.
|
|
||||||
|
|
||||||
These discrepancies mean that a `ChatHistoryProvider`-managed conversation and a service-managed conversation can diverge in content and structure, even when processing the same interactions.
|
|
||||||
|
|
||||||
### Practical Impact: Resuming After Tool-Call Termination
|
|
||||||
|
|
||||||
Today, users of `AIAgent` get different behaviors depending on whether chat history is stored service-side or in a `ChatHistoryProvider`. This creates concrete challenges — for example, when the function call loop is terminated and the user wants to resume the conversation in a subsequent run. With service-stored history, the trailing `FunctionResultContent` is never persisted, so the last stored message is the `FunctionCallContent` from the service. With `ChatHistoryProvider`-stored history, the trailing `FunctionResultContent` *is* persisted. The user cannot know whether the last `FunctionResultContent` is in the chat history or not without inspecting the storage mechanism, making it difficult to write resumption logic that works correctly regardless of the storage backend.
|
|
||||||
|
|
||||||
### Relationship Between the Two Discrepancies
|
|
||||||
|
|
||||||
The persistence timing and `FunctionResultContent` trimming behaviors are interrelated:
|
|
||||||
|
|
||||||
- **Per-service-call persistence**: When messages are persisted after each individual service call, trailing `FunctionResultContent` trimming is unnecessary. If tool calling is terminated, the `FunctionResultContent` from the terminated call was never sent to a subsequent service call, so it is never persisted. The per-service-call approach naturally matches the service's behavior.
|
|
||||||
|
|
||||||
- **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored.
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
- **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history.
|
|
||||||
- **B. Atomicity**: A run that fails mid-way through a multi-step tool-calling loop should not leave chat history in a partially-updated state, unless the user explicitly opts into that behavior.
|
|
||||||
- **C. Recoverability**: For long-running tool-calling loops, it should be possible to recover intermediate progress if the process is interrupted, rather than losing all work from the current run.
|
|
||||||
- **D. Simplicity**: The default behavior should be easy to understand and predict for most users, without requiring knowledge of the FIC loop internals.
|
|
||||||
- **E. Flexibility**: Regardless of the chosen default, users should be able to opt into the alternative behavior.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming
|
|
||||||
- Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
|
||||||
|
|
||||||
## Pros and Cons of the Options
|
|
||||||
|
|
||||||
### Option 1: Per-run persistence with opt-in FRC trimming
|
|
||||||
|
|
||||||
Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as an opt-in behavior to improve consistency with service storage.
|
|
||||||
|
|
||||||
- Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B.
|
|
||||||
- Good, because the mental model is simple: one run = one history update, satisfying driver D.
|
|
||||||
- Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A.
|
|
||||||
- Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A.
|
|
||||||
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C.
|
|
||||||
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
|
|
||||||
|
|
||||||
### Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
|
|
||||||
|
|
||||||
Introduce an optional RequirePerServiceCallChatHistoryPersistence setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
|
|
||||||
|
|
||||||
Settings:
|
|
||||||
- `RequirePerServiceCallChatHistoryPersistence` = `true`
|
|
||||||
|
|
||||||
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A.
|
|
||||||
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
|
|
||||||
- Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity.
|
|
||||||
- Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`.
|
|
||||||
- Bad, because the mental model is more complex: a single run may produce multiple history updates, partially failing driver D.
|
|
||||||
- Neutral, because users can opt out to per-run persistence if they prefer atomicity, satisfying driver E.
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
Chosen option: **Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `RequirePerServiceCallChatHistoryPersistence` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
|
|
||||||
|
|
||||||
### Configuration Matrix
|
|
||||||
|
|
||||||
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `RequirePerServiceCallChatHistoryPersistence`:
|
|
||||||
|
|
||||||
| `UseProvidedChatClientAsIs` | `RequirePerServiceCallChatHistoryPersistence` | Behavior |
|
|
||||||
|---|---|---|
|
|
||||||
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. |
|
|
||||||
| `false` | `true` | **Per-service-call persistence (simulated).** A `PerServiceCallChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
|
|
||||||
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. |
|
|
||||||
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `PerServiceCallChatHistoryPersistingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
|
|
||||||
|
|
||||||
### Consequences
|
|
||||||
|
|
||||||
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B.
|
|
||||||
- Good, because the default mental model is simple: one run = one history update, satisfying driver D.
|
|
||||||
- Good, because users who opt into `RequirePerServiceCallChatHistoryPersistence` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
|
|
||||||
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in.
|
|
||||||
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled.
|
|
||||||
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`.
|
|
||||||
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
|
|
||||||
- Neutral, because users who want per-service-call consistency can opt in via `RequirePerServiceCallChatHistoryPersistence = true`, satisfying driver E.
|
|
||||||
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
|
|
||||||
|
|
||||||
### Implementation Notes
|
|
||||||
|
|
||||||
#### Conversation ID Consistency
|
|
||||||
|
|
||||||
When `RequirePerServiceCallChatHistoryPersistence` is enabled, the `PerServiceCallChatHistoryPersistingChatClient`
|
|
||||||
decorator also updates `session.ConversationId` after each service call. This handles two scenarios:
|
|
||||||
|
|
||||||
1. **Framework-managed chat history** — the decorator sets a sentinel `ConversationId` on the response
|
|
||||||
so that `FunctionInvokingChatClient` treats the conversation as service-managed (clearing accumulated
|
|
||||||
history between iterations and not injecting duplicate `FunctionCallContent` during approval processing).
|
|
||||||
|
|
||||||
2. **Service-stored chat history** — when the service returns a real `ConversationId`, the decorator
|
|
||||||
updates `session.ConversationId` immediately after each service call, rather than deferring the update
|
|
||||||
to the end of the run. This ensures intermediate ConversationId changes are captured even if the
|
|
||||||
process is interrupted mid-loop.
|
|
||||||
|
|
||||||
For some service-stored scenarios (e.g., the Conversations API with the Responses API), there is only
|
|
||||||
one thread with one ID, so every service call returns the same ConversationId and this per-call update
|
|
||||||
makes no practical difference. Enabling `RequirePerServiceCallChatHistoryPersistence` ensures consistent
|
|
||||||
per-service-call behavior across all service types regardless of how they manage ConversationIds.
|
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ This feature ports the vector store abstractions, embedding generator abstractio
|
|||||||
**Goal:** Add embedding generators to all existing AF provider packages that have chat clients.
|
**Goal:** Add embedding generators to all existing AF provider packages that have chat clients.
|
||||||
**Mergeable:** Yes — each is independent, added to existing provider packages.
|
**Mergeable:** Yes — each is independent, added to existing provider packages.
|
||||||
|
|
||||||
#### 2.1 — Foundry inference embedding (in `packages/foundry/`)
|
#### 2.1 — Azure AI Inference embedding (in `packages/azure-ai/`)
|
||||||
#### 2.2 — Ollama embedding (in `packages/ollama/`)
|
#### 2.2 — Ollama embedding (in `packages/ollama/`)
|
||||||
#### 2.3 — Anthropic embedding (in `packages/anthropic/`)
|
#### 2.3 — Anthropic embedding (in `packages/anthropic/`)
|
||||||
#### 2.4 — Bedrock embedding (in `packages/bedrock/`)
|
#### 2.4 — Bedrock embedding (in `packages/bedrock/`)
|
||||||
|
|||||||
+2
-2
@@ -12,8 +12,8 @@ dotnet/
|
|||||||
│ ├── Microsoft.Agents.AI.Abstractions/ # Core AI agent abstractions
|
│ ├── Microsoft.Agents.AI.Abstractions/ # Core AI agent abstractions
|
||||||
│ ├── Microsoft.Agents.AI.A2A/ # Agent-to-Agent (A2A) provider
|
│ ├── Microsoft.Agents.AI.A2A/ # Agent-to-Agent (A2A) provider
|
||||||
│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider
|
│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider
|
||||||
│ ├── Microsoft.Agents.AI.Foundry/ # Microsoft Foundry Agents (v2) provider
|
│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI Foundry Agents (v2) provider
|
||||||
│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Microsoft Foundry Agents (v1) provider
|
│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Azure AI Foundry Agents (v1) provider
|
||||||
│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider
|
│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider
|
||||||
│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration
|
│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration
|
||||||
│ └── ... # Other packages
|
│ └── ... # Other packages
|
||||||
|
|||||||
-213
@@ -1,213 +0,0 @@
|
|||||||
---
|
|
||||||
name: verify-samples-tool
|
|
||||||
description: How to use the verify-samples tool to run, verify, and manage sample definitions in the Agent Framework repository. Use this when adding, updating, or running sample verification.
|
|
||||||
---
|
|
||||||
|
|
||||||
# verify-samples Tool
|
|
||||||
|
|
||||||
The `verify-samples` project (`dotnet/eng/verify-samples/`) is an automated tool that runs sample projects and verifies their output using deterministic checks and AI-powered verification.
|
|
||||||
|
|
||||||
## Running verify-samples
|
|
||||||
|
|
||||||
```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.",
|
|
||||||
},
|
|
||||||
```
|
|
||||||
+1
-2
@@ -29,8 +29,7 @@ using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, `AIFunct
|
|||||||
|
|
||||||
## Key Conventions
|
## Key Conventions
|
||||||
|
|
||||||
- **Command output capture**: When running `dotnet build`, `dotnet test`, `dotnet format`, or similar commands, redirect output to a temp file first (e.g., `dotnet build --tl:off 2>&1 | Out-File $env:TEMP\build.log`), then analyze the file as needed. This avoids re-running expensive commands when the initial analysis misses something.
|
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly.
|
||||||
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly. When using PowerShell `Set-Content`, always pass `-Encoding UTF8BOM` to preserve the BOM (e.g., `Set-Content $file $content -NoNewline -Encoding UTF8BOM`).
|
|
||||||
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
|
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
|
||||||
- **XML docs**: Required for all public methods and classes
|
- **XML docs**: Required for all public methods and classes
|
||||||
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
|
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<IsReleaseCandidate>false</IsReleaseCandidate>
|
<IsReleaseCandidate>false</IsReleaseCandidate>
|
||||||
<IsReleased>false</IsReleased>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||||
<!-- Azure.* -->
|
<!-- Azure.* -->
|
||||||
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0" />
|
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0-beta.2" />
|
||||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
||||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||||
<PackageVersion Include="Azure.Identity" Version="1.20.0" />
|
<PackageVersion Include="Azure.Identity" Version="1.19.0" />
|
||||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||||
<!-- Google Gemini -->
|
<!-- Google Gemini -->
|
||||||
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
|
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
|
||||||
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
||||||
<!-- Microsoft.Azure.* -->
|
<!-- Microsoft.Azure.* -->
|
||||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
|
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.4" />
|
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.4" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.4" />
|
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.4" />
|
||||||
<PackageVersion Include="System.ClientModel" Version="1.10.0" />
|
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
|
||||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
||||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Configurations>
|
<Configurations>
|
||||||
<BuildType Name="Debug" />
|
<BuildType Name="Debug" />
|
||||||
<BuildType Name="Publish" />
|
<BuildType Name="Publish" />
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
<Folder Name="/Samples/">
|
<Folder Name="/Samples/">
|
||||||
<File Path="samples/AGENTS.md" />
|
<File Path="samples/AGENTS.md" />
|
||||||
<File Path="samples/README.md" />
|
<File Path="samples/README.md" />
|
||||||
<Project Path="eng/verify-samples/verify-samples.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/01-get-started/">
|
<Folder Name="/Samples/01-get-started/">
|
||||||
<Project Path="samples/01-get-started/01_hello_agent/01_hello_agent.csproj" />
|
<Project Path="samples/01-get-started/01_hello_agent/01_hello_agent.csproj" />
|
||||||
@@ -34,6 +33,7 @@
|
|||||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
<Project Path="samples/02-agents/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
<Project Path="samples/02-agents/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
<Project Path="samples/02-agents/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
||||||
|
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIAssistants/Agent_With_OpenAIAssistants.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
|
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
|
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
|
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
|
||||||
<Project Path="samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Agent_Step17_AdditionalAIContext.csproj" />
|
<Project Path="samples/02-agents/Agents/Agent_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_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj" />
|
||||||
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
|
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
|
||||||
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||||
@@ -77,8 +76,6 @@
|
|||||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
<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/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||||
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.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>
|
||||||
<Folder Name="/Samples/GettingStarted/">
|
<Folder Name="/Samples/GettingStarted/">
|
||||||
<File Path="samples/GettingStarted/README.md" />
|
<File Path="samples/GettingStarted/README.md" />
|
||||||
@@ -104,8 +101,7 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/AgentSkills/">
|
<Folder Name="/Samples/02-agents/AgentSkills/">
|
||||||
<File Path="samples/02-agents/AgentSkills/README.md" />
|
<File Path="samples/02-agents/AgentSkills/README.md" />
|
||||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
|
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||||
@@ -122,34 +118,6 @@
|
|||||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
|
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
|
||||||
</Folder>
|
</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/">
|
<Folder Name="/Samples/02-agents/AgentWithMemory/">
|
||||||
<File Path="samples/02-agents/AgentWithMemory/README.md" />
|
<File Path="samples/02-agents/AgentWithMemory/README.md" />
|
||||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||||
@@ -171,7 +139,35 @@
|
|||||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
|
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.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" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
||||||
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
||||||
@@ -213,12 +209,12 @@
|
|||||||
<Project Path="samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj" />
|
<Project Path="samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/03-workflows/Declarative/Examples/">
|
<Folder Name="/Samples/03-workflows/Declarative/Examples/">
|
||||||
<File Path="../declarative-agents/workflow-samples/CustomerSupport.yaml" />
|
<File Path="../workflow-samples/CustomerSupport.yaml" />
|
||||||
<File Path="../declarative-agents/workflow-samples/DeepResearch.yaml" />
|
<File Path="../workflow-samples/DeepResearch.yaml" />
|
||||||
<File Path="../declarative-agents/workflow-samples/Marketing.yaml" />
|
<File Path="../workflow-samples/Marketing.yaml" />
|
||||||
<File Path="../declarative-agents/workflow-samples/MathChat.yaml" />
|
<File Path="../workflow-samples/MathChat.yaml" />
|
||||||
<File Path="../declarative-agents/workflow-samples/README.md" />
|
<File Path="../workflow-samples/README.md" />
|
||||||
<File Path="../declarative-agents/workflow-samples/wttr.json" />
|
<File Path="../workflow-samples/wttr.json" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/03-workflows/SharedStates/">
|
<Folder Name="/Samples/03-workflows/SharedStates/">
|
||||||
<Project Path="samples/03-workflows/SharedStates/SharedStates.csproj" />
|
<Project Path="samples/03-workflows/SharedStates/SharedStates.csproj" />
|
||||||
@@ -319,8 +315,8 @@
|
|||||||
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
|
<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/docker-compose.yml" />
|
||||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
|
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
|
||||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj" />
|
|
||||||
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
|
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
|
||||||
|
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Solution Items/">
|
<Folder Name="/Solution Items/">
|
||||||
<File Path=".editorconfig" />
|
<File Path=".editorconfig" />
|
||||||
@@ -477,13 +473,13 @@
|
|||||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||||
|
<Project Path="src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||||
@@ -494,7 +490,7 @@
|
|||||||
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
||||||
@@ -505,11 +501,11 @@
|
|||||||
<Folder Name="/Tests/IntegrationTests/">
|
<Folder Name="/Tests/IntegrationTests/">
|
||||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||||
|
<Project Path="tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||||
@@ -525,12 +521,12 @@
|
|||||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||||
|
<Project Path="tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
|
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
|
||||||
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
|
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
|
||||||
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
|
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Foundry\\Microsoft.Agents.AI.Foundry.csproj",
|
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
|
||||||
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
|
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
|
||||||
"src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
|
"src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
|
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
|
||||||
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
|
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
|
||||||
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
|
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
|
||||||
|
"src\\Microsoft.Agents.AI.FoundryMemory\\Microsoft.Agents.AI.FoundryMemory.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
||||||
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
|
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
|
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
namespace VerifySamples;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Thread-safe console output with sample-name prefixes and colored status.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ConsoleReporter
|
|
||||||
{
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a complete prefixed line atomically to the console.
|
|
||||||
/// </summary>
|
|
||||||
public void WriteLineWithPrefix(string sampleName, string message, ConsoleColor? color = null)
|
|
||||||
{
|
|
||||||
lock (this._lock)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
|
||||||
Console.Write($"[{sampleName}] ");
|
|
||||||
if (color.HasValue)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = color.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine(message);
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints the final summary table and elapsed time to the console.
|
|
||||||
/// </summary>
|
|
||||||
public void PrintSummary(
|
|
||||||
IReadOnlyList<VerificationResult> orderedResults,
|
|
||||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
|
||||||
TimeSpan elapsed)
|
|
||||||
{
|
|
||||||
var passCount = orderedResults.Count(r => r.Passed);
|
|
||||||
var failCount = orderedResults.Count(r => !r.Passed);
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(new string('─', 60));
|
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
|
||||||
Console.WriteLine("SUMMARY");
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
foreach (var result in orderedResults)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = result.Passed ? ConsoleColor.Green : ConsoleColor.Red;
|
|
||||||
Console.Write(result.Passed ? " ✓ " : " ✗ ");
|
|
||||||
Console.ResetColor();
|
|
||||||
Console.WriteLine($"{result.SampleName}: {result.Summary}");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (name, reason) in skipped)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
||||||
Console.Write(" ○ ");
|
|
||||||
Console.ResetColor();
|
|
||||||
Console.WriteLine($"{name}: Skipped — {reason}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.Write("Results: ");
|
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
|
||||||
Console.Write($"{passCount} passed");
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
if (failCount > 0)
|
|
||||||
{
|
|
||||||
Console.Write(", ");
|
|
||||||
Console.ForegroundColor = ConsoleColor.Red;
|
|
||||||
Console.Write($"{failCount} failed");
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skipped.Count > 0)
|
|
||||||
{
|
|
||||||
Console.Write(", ");
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
||||||
Console.Write($"{skipped.Count} skipped");
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
|
||||||
Console.WriteLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace VerifySamples;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a CSV summary of sample verification results.
|
|
||||||
/// </summary>
|
|
||||||
internal static class CsvResultWriter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the results to a CSV file at the specified path.
|
|
||||||
/// </summary>
|
|
||||||
public static async Task WriteAsync(
|
|
||||||
string path,
|
|
||||||
IReadOnlyList<VerificationResult> orderedResults,
|
|
||||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
|
||||||
IReadOnlyList<SampleDefinition> samples)
|
|
||||||
{
|
|
||||||
var pathLookup = samples.ToDictionary(s => s.Name, s => s.ProjectPath);
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("Sample,ProjectPath,Status,FailedChecks,Failures");
|
|
||||||
|
|
||||||
foreach (var result in orderedResults)
|
|
||||||
{
|
|
||||||
var status = result.Passed ? "PASSED" : "FAILED";
|
|
||||||
var failedChecks = result.Failures.Count;
|
|
||||||
var failures = string.Join("; ", result.Failures);
|
|
||||||
pathLookup.TryGetValue(result.SampleName, out var projectPath);
|
|
||||||
sb.AppendLine($"{CsvEscape(result.SampleName)},{CsvEscape(projectPath ?? "")},{status},{failedChecks},{CsvEscape(failures)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (name, reason) in skipped)
|
|
||||||
{
|
|
||||||
pathLookup.TryGetValue(name, out var projectPath);
|
|
||||||
sb.AppendLine($"{CsvEscape(name)},{CsvEscape(projectPath ?? "")},SKIPPED,0,{CsvEscape(reason)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(path, sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Escapes a value for CSV: wraps in quotes if it contains commas, quotes, or newlines.
|
|
||||||
/// </summary>
|
|
||||||
private static string CsvEscape(string value)
|
|
||||||
{
|
|
||||||
if (value.Contains('"') || value.Contains(',') || value.Contains('\n') || value.Contains('\r'))
|
|
||||||
{
|
|
||||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
namespace VerifySamples;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the expected behavior for each sample in 01-get-started.
|
|
||||||
/// </summary>
|
|
||||||
internal static class GetStartedSamples
|
|
||||||
{
|
|
||||||
public static IReadOnlyList<SampleDefinition> All { get; } =
|
|
||||||
[
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "05_first_workflow",
|
|
||||||
ProjectPath = "samples/01-get-started/05_first_workflow",
|
|
||||||
RequiredEnvironmentVariables = [],
|
|
||||||
IsDeterministic = true,
|
|
||||||
MustContain =
|
|
||||||
[
|
|
||||||
"UppercaseExecutor: HELLO, WORLD!",
|
|
||||||
"ReverseTextExecutor: !DLROW ,OLLEH",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "01_hello_agent",
|
|
||||||
ProjectPath = "samples/01-get-started/01_hello_agent",
|
|
||||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
|
||||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
|
||||||
ExpectedOutputDescription =
|
|
||||||
[
|
|
||||||
"The output should contain a joke about a pirate.",
|
|
||||||
"There should be two separate joke responses — one from a non-streaming call and one from a streaming call.",
|
|
||||||
"The output should not contain error messages or stack traces.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "02_add_tools",
|
|
||||||
ProjectPath = "samples/01-get-started/02_add_tools",
|
|
||||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
|
||||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
|
||||||
MustContain = [],
|
|
||||||
ExpectedOutputDescription =
|
|
||||||
[
|
|
||||||
"The output should contain information about the weather in Amsterdam.",
|
|
||||||
"The response should mention that it is cloudy with a high of 15°C (or equivalent), since this comes from a tool that returns a canned response.",
|
|
||||||
"There should be two responses — one from a non-streaming call and one from a streaming call.",
|
|
||||||
"The output should not contain error messages or stack traces.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "03_multi_turn",
|
|
||||||
ProjectPath = "samples/01-get-started/03_multi_turn",
|
|
||||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
|
||||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
|
||||||
ExpectedOutputDescription =
|
|
||||||
[
|
|
||||||
"The output should contain a joke about a pirate.",
|
|
||||||
"After the initial joke, there should be a modified version that includes emojis and is told in the voice of a pirate's parrot.",
|
|
||||||
"The pattern repeats: first a non-streaming pirate joke + parrot version, then a streaming pirate joke + parrot version.",
|
|
||||||
"The output should not contain error messages or stack traces.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "04_memory",
|
|
||||||
ProjectPath = "samples/01-get-started/04_memory",
|
|
||||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
|
||||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
|
||||||
MustContain =
|
|
||||||
[
|
|
||||||
">> Use session with blank memory",
|
|
||||||
">> Use deserialized session with previously created memories",
|
|
||||||
">> Read memories using memory component",
|
|
||||||
"MEMORY - User Name:",
|
|
||||||
"MEMORY - User Age:",
|
|
||||||
">> Use new session with previously created memories",
|
|
||||||
],
|
|
||||||
ExpectedOutputDescription =
|
|
||||||
[
|
|
||||||
"In the 'Use session with blank memory' section, the agent should respond to the user's messages. It may ask for the user's name or age if not yet known.",
|
|
||||||
"In the 'Use deserialized session with previously created memories' section, the agent should correctly recall that the user's name is Ruaidhrí and age is 20.",
|
|
||||||
"The 'MEMORY - User Name:' line should show 'Ruaidhrí' (or a close transliteration).",
|
|
||||||
"The 'MEMORY - User Age:' line should show '20'.",
|
|
||||||
"In the 'Use new session with previously created memories' section, the agent should know the user's name and age from the transferred memory.",
|
|
||||||
"The output should not contain error messages or stack traces.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "06_host_your_agent",
|
|
||||||
ProjectPath = "samples/01-get-started/06_host_your_agent",
|
|
||||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
|
||||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
|
||||||
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace VerifySamples;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Incrementally writes a sequential (non-interleaved) log file, appending after each sample completes.
|
|
||||||
/// Thread-safe: multiple parallel tasks may call write methods concurrently.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class LogFileWriter : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _path;
|
|
||||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
|
||||||
|
|
||||||
public LogFileWriter(string path)
|
|
||||||
{
|
|
||||||
this._path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
this._writeLock.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the log file header. Call once at the start of the run.
|
|
||||||
/// </summary>
|
|
||||||
public async Task WriteHeaderAsync()
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"Sample Verification Log — {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
|
||||||
sb.AppendLine(new string('═', 72));
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(this._path, sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Appends a skipped-sample entry to the log file.
|
|
||||||
/// </summary>
|
|
||||||
public async Task WriteSkippedAsync(string name, string reason)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"── {name} ──");
|
|
||||||
sb.AppendLine($"Status: SKIPPED — {reason}");
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
await this.AppendAsync(sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Appends a completed sample's full output section to the log file.
|
|
||||||
/// </summary>
|
|
||||||
public async Task WriteSampleResultAsync(VerificationResult result)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine(new string('─', 72));
|
|
||||||
sb.AppendLine($"── {result.SampleName} ──");
|
|
||||||
sb.AppendLine($"Status: {(result.Passed ? "PASSED" : "FAILED")}");
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
foreach (var line in result.LogLines)
|
|
||||||
{
|
|
||||||
sb.AppendLine(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.Stdout))
|
|
||||||
{
|
|
||||||
sb.AppendLine("--- stdout ---");
|
|
||||||
sb.AppendLine(result.Stdout.TrimEnd());
|
|
||||||
sb.AppendLine("--- end stdout ---");
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.Stderr))
|
|
||||||
{
|
|
||||||
sb.AppendLine("--- stderr ---");
|
|
||||||
sb.AppendLine(result.Stderr.TrimEnd());
|
|
||||||
sb.AppendLine("--- end stderr ---");
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Failures.Count > 0)
|
|
||||||
{
|
|
||||||
sb.AppendLine("Failures:");
|
|
||||||
foreach (var failure in result.Failures)
|
|
||||||
{
|
|
||||||
sb.AppendLine($" ✗ {failure}");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.AIReasoning is not null)
|
|
||||||
{
|
|
||||||
sb.AppendLine("AI Reasoning:");
|
|
||||||
sb.AppendLine(result.AIReasoning);
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.AppendAsync(sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Appends the final summary section and elapsed time to the log file.
|
|
||||||
/// </summary>
|
|
||||||
public async Task WriteSummaryAsync(
|
|
||||||
IReadOnlyList<VerificationResult> orderedResults,
|
|
||||||
IReadOnlyList<(string Name, string Reason)> skipped,
|
|
||||||
TimeSpan elapsed)
|
|
||||||
{
|
|
||||||
var passCount = orderedResults.Count(r => r.Passed);
|
|
||||||
var failCount = orderedResults.Count(r => !r.Passed);
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine(new string('═', 72));
|
|
||||||
sb.AppendLine("SUMMARY");
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
foreach (var result in orderedResults)
|
|
||||||
{
|
|
||||||
sb.AppendLine($" {(result.Passed ? "✓" : "✗")} {result.SampleName}: {result.Summary}");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (name, reason) in skipped)
|
|
||||||
{
|
|
||||||
sb.AppendLine($" ○ {name}: Skipped — {reason}");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine($"Results: {passCount} passed{(failCount > 0 ? $", {failCount} failed" : "")}{(skipped.Count > 0 ? $", {skipped.Count} skipped" : "")}");
|
|
||||||
sb.AppendLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
|
|
||||||
|
|
||||||
await this.AppendAsync(sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AppendAsync(string text)
|
|
||||||
{
|
|
||||||
await this._writeLock.WaitAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await File.AppendAllTextAsync(this._path, text);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this._writeLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
namespace VerifySamples;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes a sample to verify, including its expected output.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class SampleDefinition
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Display name for the sample (e.g., "01_hello_agent").
|
|
||||||
/// </summary>
|
|
||||||
public required string Name { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Relative path from the dotnet/ directory to the sample project directory.
|
|
||||||
/// </summary>
|
|
||||||
public required string ProjectPath { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Environment variables that the sample requires for a meaningful run.
|
|
||||||
/// The runner checks these before running and will skip the sample if any are unset,
|
|
||||||
/// recording a skip reason that indicates which required variables are missing.
|
|
||||||
/// </summary>
|
|
||||||
public string[] RequiredEnvironmentVariables { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Environment variables that the sample can use but typically has fallbacks or defaults for.
|
|
||||||
/// If these are not set, the sample might prompt or behave interactively, which could cause
|
|
||||||
/// automated verification to hang. The runner checks these and skips the sample if they are unset
|
|
||||||
/// to avoid non-deterministic or blocking behavior in automated runs.
|
|
||||||
/// </summary>
|
|
||||||
public string[] OptionalEnvironmentVariables { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If set, the sample is skipped with this reason.
|
|
||||||
/// Use only for structural reasons (e.g., web server, multi-process, needs external service).
|
|
||||||
/// Do NOT use for missing environment variables — those are checked dynamically.
|
|
||||||
/// </summary>
|
|
||||||
public string? SkipReason { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Substrings that must appear in stdout for the sample to pass.
|
|
||||||
/// Used for deterministic verification.
|
|
||||||
/// </summary>
|
|
||||||
public string[] MustContain { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Substrings that must not appear in stdout for the sample to pass.
|
|
||||||
/// </summary>
|
|
||||||
public string[] MustNotContain { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, <see cref="MustContain"/> entries cover the entire expected output —
|
|
||||||
/// no AI verification is needed.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsDeterministic { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Natural-language description of what the sample output should look like.
|
|
||||||
/// Used by the AI verifier for non-deterministic samples.
|
|
||||||
/// Each entry describes one aspect of the expected output that should be verified.
|
|
||||||
/// </summary>
|
|
||||||
public string[] ExpectedOutputDescription { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sequence of stdin inputs to feed to the sample process.
|
|
||||||
/// Each entry is written as a line (followed by newline) to the process stdin.
|
|
||||||
/// A <c>null</c> entry inserts a delay without writing anything.
|
|
||||||
/// Inputs are sent with a short delay between each to allow the process to prompt.
|
|
||||||
/// </summary>
|
|
||||||
public string?[] Inputs { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delay in milliseconds between each input line. Default is 2000ms.
|
|
||||||
/// Increase for samples that need more time between prompts (e.g., LLM calls between inputs).
|
|
||||||
/// </summary>
|
|
||||||
public int InputDelayMs { get; init; } = 2000;
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace VerifySamples;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Result of running a sample process.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed record SampleRunResult(
|
|
||||||
string Stdout,
|
|
||||||
string Stderr,
|
|
||||||
int ExitCode,
|
|
||||||
TimeSpan Elapsed);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs a sample project via <c>dotnet run</c> and captures its output.
|
|
||||||
/// </summary>
|
|
||||||
internal static class SampleRunner
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Runs <c>dotnet run --framework net10.0</c> in the given project directory.
|
|
||||||
/// </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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
// 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; }
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
// 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] + "...";
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// 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; } = [];
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,525 +0,0 @@
|
|||||||
// 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."],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<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.Identity" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -2,20 +2,17 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Central version prefix - applies to all nuget packages. -->
|
<!-- Central version prefix - applies to all nuget packages. -->
|
||||||
<VersionPrefix>1.0.0</VersionPrefix>
|
<VersionPrefix>1.0.0</VersionPrefix>
|
||||||
<RCNumber>6</RCNumber>
|
<RCNumber>4</RCNumber>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260402.1</PackageVersion>
|
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260311.1</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260402.1</PackageVersion>
|
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260311.1</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
<GitTag>1.0.0-rc4</GitTag>
|
||||||
<GitTag>1.0.0</GitTag>
|
|
||||||
|
|
||||||
<Configurations>Debug;Release;Publish</Configurations>
|
<Configurations>Debug;Release;Publish</Configurations>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
|
|
||||||
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
|
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
|
||||||
<PackageValidationBaselineVersion>1.0.0-rc5</PackageValidationBaselineVersion>
|
<PackageValidationBaselineVersion>0.0.1</PackageValidationBaselineVersion>
|
||||||
<!-- Enable validation for RC packages and GA packages -->
|
|
||||||
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsReleased)' == 'true'">true</EnablePackageValidation>
|
|
||||||
<!-- Validate assembly attributes only for Publish builds -->
|
<!-- Validate assembly attributes only for Publish builds -->
|
||||||
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
|
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
|
||||||
<!-- Do not validate reference assemblies -->
|
<!-- Do not validate reference assemblies -->
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
|
|||||||
|
|
||||||
if (approvalRequest.AdditionalProperties != null)
|
if (approvalRequest.AdditionalProperties != null)
|
||||||
{
|
{
|
||||||
approvalResponse.AdditionalProperties = [];
|
approvalResponse.AdditionalProperties = new AdditionalPropertiesDictionary();
|
||||||
foreach (var kvp in approvalRequest.AdditionalProperties)
|
foreach (var kvp in approvalRequest.AdditionalProperties)
|
||||||
{
|
{
|
||||||
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
|
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
|
||||||
|
|||||||
+4
-4
@@ -131,9 +131,9 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
|||||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||||
approvalCalls.Remove(functionResult.CallId);
|
approvalCalls.Remove(functionResult.CallId);
|
||||||
}
|
}
|
||||||
else
|
else if (transformedContents != null)
|
||||||
{
|
{
|
||||||
transformedContents?.Add(content);
|
transformedContents.Add(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +155,10 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
|||||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||||
result.Add(newMessage);
|
result.Add(newMessage);
|
||||||
}
|
}
|
||||||
else
|
else if (result != null)
|
||||||
{
|
{
|
||||||
// We're already copying messages, so copy this unchanged message too
|
// 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
|
// If result is null, we haven't made any changes yet, so keep processing
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-8
@@ -57,10 +57,16 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
|||||||
throw new InvalidOperationException("Invalid request_approval tool call");
|
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 &&
|
reqObj is JsonElement argsElement &&
|
||||||
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
|
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
|
||||||
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
|
approvalRequest != null ? approvalRequest : null;
|
||||||
|
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to deserialize approval request from tool call");
|
||||||
|
}
|
||||||
|
|
||||||
return new ToolApprovalRequestContent(
|
return new ToolApprovalRequestContent(
|
||||||
requestId: request.ApprovalId,
|
requestId: request.ApprovalId,
|
||||||
new FunctionCallContent(
|
new FunctionCallContent(
|
||||||
@@ -71,11 +77,17 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
|||||||
|
|
||||||
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
|
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))) :
|
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||||
result.Result is string str ?
|
result.Result is string str ?
|
||||||
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||||
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
|
result.Result as ApprovalResponse;
|
||||||
|
|
||||||
|
if (approvalResponse == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to deserialize approval response from tool result");
|
||||||
|
}
|
||||||
|
|
||||||
return approval.CreateResponse(approvalResponse.Approved);
|
return approval.CreateResponse(approvalResponse.Approved);
|
||||||
}
|
}
|
||||||
#pragma warning restore MEAI001
|
#pragma warning restore MEAI001
|
||||||
@@ -109,7 +121,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
|||||||
// Track approval ID to original call ID mapping
|
// Track approval ID to original call ID mapping
|
||||||
_ = new Dictionary<string, string>();
|
_ = 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.
|
#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 = []; // Remote approvals
|
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = new(); // Remote approvals
|
||||||
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||||
{
|
{
|
||||||
var message = messages[messageIndex];
|
var message = messages[messageIndex];
|
||||||
@@ -134,7 +146,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (content is FunctionResultContent toolResult &&
|
else if (content is FunctionResultContent toolResult &&
|
||||||
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
|
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval) == true)
|
||||||
{
|
{
|
||||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
||||||
@@ -149,9 +161,9 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
|||||||
AdditionalProperties = message.AdditionalProperties
|
AdditionalProperties = message.AdditionalProperties
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else if (result != null)
|
||||||
{
|
{
|
||||||
result?.Add(message);
|
result.Add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
|
|||||||
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
|
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
|
||||||
{
|
{
|
||||||
// Deserialize the state
|
// Deserialize the state
|
||||||
if (JsonSerializer.Deserialize(
|
TState? newState = JsonSerializer.Deserialize(
|
||||||
dataContent.Data.Span,
|
dataContent.Data.Span,
|
||||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
|
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) as TState;
|
||||||
|
if (newState != null)
|
||||||
{
|
{
|
||||||
this.State = newState;
|
this.State = newState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,11 @@ using OpenTelemetry.Trace;
|
|||||||
|
|
||||||
#region Setup Telemetry
|
#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 SourceName = "OpenTelemetryAspire.ConsoleApp";
|
||||||
const string ServiceName = "AgentOpenTelemetry";
|
const string ServiceName = "AgentOpenTelemetry";
|
||||||
|
|
||||||
// Configure OpenTelemetry for Aspire dashboard
|
// Configure OpenTelemetry for Aspire dashboard
|
||||||
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4317";
|
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4318";
|
||||||
|
|
||||||
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
|
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
|
||||||
|
|
||||||
@@ -41,6 +40,7 @@ var resource = ResourceBuilder.CreateDefault()
|
|||||||
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
|
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
|
||||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
||||||
.AddSource(SourceName) // Our custom activity source
|
.AddSource(SourceName) // Our custom activity source
|
||||||
|
.AddSource("*Microsoft.Agents.AI") // Agent Framework telemetry
|
||||||
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
|
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
|
||||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
|
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
|
||||||
|
|
||||||
@@ -54,7 +54,8 @@ using var tracerProvider = tracerProviderBuilder.Build();
|
|||||||
// Setup metrics with resource and instrument name filtering
|
// Setup metrics with resource and instrument name filtering
|
||||||
using var meterProvider = Sdk.CreateMeterProviderBuilder()
|
using var meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
||||||
.AddMeter(SourceName) // Our custom meter source
|
.AddMeter(SourceName) // Our custom meter
|
||||||
|
.AddMeter("*Microsoft.Agents.AI") // Agent Framework metrics
|
||||||
.AddHttpClientInstrumentation() // HTTP client metrics
|
.AddHttpClientInstrumentation() // HTTP client metrics
|
||||||
.AddRuntimeInstrumentation() // .NET runtime metrics
|
.AddRuntimeInstrumentation() // .NET runtime metrics
|
||||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
|
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
|
||||||
@@ -127,7 +128,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
|
|||||||
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
||||||
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
|
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
|
||||||
.AsBuilder()
|
.AsBuilder()
|
||||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var session = await agent.CreateSessionAsync();
|
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:
|
The sample supports three deployment scenarios:
|
||||||
|
|
||||||
1. **Anthropic Public API** - Direct connection to Anthropic's public API
|
1. **Anthropic Public API** - Direct connection to Anthropic's public API
|
||||||
2. **Microsoft Foundry with API Key** - Anthropic models deployed through Microsoft Foundry using API key authentication
|
2. **Azure Foundry with API Key** - Anthropic models deployed through Azure Foundry using API key authentication
|
||||||
3. **Microsoft Foundry with Azure CLI** - Anthropic models deployed through Microsoft Foundry using Azure CLI credentials
|
3. **Azure Foundry with Azure CLI** - Anthropic models deployed through Azure Foundry using Azure CLI credentials
|
||||||
|
|
||||||
## Prerequisites
|
## 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
|
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Microsoft Foundry with API Key
|
### For Azure Foundry with API Key
|
||||||
|
|
||||||
- Microsoft Foundry service endpoint and deployment configured
|
- Azure Foundry service endpoint and deployment configured
|
||||||
- Anthropic API key
|
- Anthropic API key
|
||||||
|
|
||||||
Set the following environment variables:
|
Set the following environment variables:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Microsoft Foundry resource name (subdomain before .services.ai.azure.com)
|
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure 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_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
|
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Microsoft Foundry with Azure CLI
|
### For Azure Foundry with Azure CLI
|
||||||
|
|
||||||
- Microsoft Foundry service endpoint and deployment configured
|
- Azure Foundry service endpoint and deployment configured
|
||||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||||
|
|
||||||
Set the following environment variables:
|
Set the following environment variables:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Microsoft Foundry resource name (subdomain before .services.ai.azure.com)
|
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure 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
|
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||||
```
|
```
|
||||||
|
|
||||||
**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).
|
**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).
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete - sample uses deprecated PersistentAgentsClientExtensions
|
#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 Microsoft Foundry Agents as the backend.
|
// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend.
|
||||||
|
|
||||||
using Azure.AI.Agents.Persistent;
|
using Azure.AI.Agents.Persistent;
|
||||||
using Azure.Identity;
|
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:
|
Before you begin, ensure you have the following prerequisites:
|
||||||
|
|
||||||
- .NET 10 SDK or later
|
- .NET 10 SDK or later
|
||||||
- Microsoft Foundry service endpoint and deployment configured
|
- Azure Foundry service endpoint and deployment configured
|
||||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
- 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).
|
**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).
|
||||||
|
|
||||||
Set the following environment variables:
|
Set the following environment variables:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$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_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_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
// This sample shows how to create and use AI agents with Microsoft Foundry Agents as the backend.
|
// This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend.
|
||||||
|
|
||||||
using Azure.AI.Projects;
|
using Azure.AI.Projects;
|
||||||
using Azure.AI.Projects.Agents;
|
using Azure.AI.Projects.Agents;
|
||||||
using Azure.Identity;
|
using Azure.Identity;
|
||||||
using Microsoft.Agents.AI;
|
using Microsoft.Agents.AI;
|
||||||
using Microsoft.Agents.AI.Foundry;
|
|
||||||
|
|
||||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
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 deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||||
|
|
||||||
const string JokerName = "JokerAgent";
|
const string JokerName = "JokerAgent";
|
||||||
|
|
||||||
// Get a client to create/retrieve/delete server side agents with Microsoft Foundry Agents.
|
// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents.
|
||||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||||
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||||
|
|
||||||
// Define the agent you want to create. (Prompt Agent in this case)
|
// Define the agent you want to create. (Prompt Agent in this case)
|
||||||
var agentVersionCreationOptions = new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition(model: deploymentName) { Instructions = "You are good at telling jokes." });
|
var agentVersionCreationOptions = new AgentVersionCreationOptions(new PromptAgentDefinition(model: deploymentName) { Instructions = "You are good at telling jokes." });
|
||||||
// Azure.AI.Agents SDK creates and manages agent by name and versions.
|
// Azure.AI.Agents SDK creates and manages agent by name and versions.
|
||||||
// You can create a server side agent version with the Azure.AI.Agents SDK client below.
|
// You can create a server side agent version with the Azure.AI.Agents SDK client below.
|
||||||
var createdAgentVersion = aiProjectClient.AgentAdministrationClient.CreateAgentVersion(agentName: JokerName, options: agentVersionCreationOptions);
|
var createdAgentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: JokerName, options: agentVersionCreationOptions);
|
||||||
|
|
||||||
// Note:
|
// Note:
|
||||||
// agentVersion.Id = "<agentName>:<versionNumber>",
|
// agentVersion.Id = "<agentName>:<versionNumber>",
|
||||||
@@ -31,18 +30,14 @@ var createdAgentVersion = aiProjectClient.AgentAdministrationClient.CreateAgentV
|
|||||||
// agentVersion.Name = <agentName>
|
// agentVersion.Name = <agentName>
|
||||||
|
|
||||||
// You can use an AIAgent with an already created server side agent version.
|
// You can use an AIAgent with an already created server side agent version.
|
||||||
FoundryAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
|
AIAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
|
||||||
|
|
||||||
// You can also create another AIAgent version by providing the same name with a different definition.
|
// You can also create another AIAgent version by providing the same name with a different definition.
|
||||||
ProjectsAgentVersion newJokerAgentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
AIAgent newJokerAgent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: "You are extremely hilarious at telling jokes.");
|
||||||
JokerName,
|
|
||||||
new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition(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.
|
// You can also get the AIAgent latest version just providing its name.
|
||||||
ProjectsAgentRecord jokerAgentRecord = await aiProjectClient.AgentAdministrationClient.GetAgentAsync(JokerName);
|
AIAgent jokerAgentLatest = await aiProjectClient.GetAIAgentAsync(name: JokerName);
|
||||||
FoundryAgent jokerAgentLatest = aiProjectClient.AsAIAgent(jokerAgentRecord);
|
var latestAgentVersion = jokerAgentLatest.GetService<AgentVersion>()!;
|
||||||
ProjectsAgentVersion latestAgentVersion = jokerAgentRecord.GetLatestVersion();
|
|
||||||
|
|
||||||
// The AIAgent version can be accessed via the GetService method.
|
// The AIAgent version can be accessed via the GetService method.
|
||||||
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
|
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
|
||||||
@@ -55,4 +50,4 @@ Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate
|
|||||||
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", session));
|
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", session));
|
||||||
|
|
||||||
// Cleanup by agent name removes both agent versions created.
|
// Cleanup by agent name removes both agent versions created.
|
||||||
aiProjectClient.AgentAdministrationClient.DeleteAgent(existingJokerAgent.Name);
|
aiProjectClient.Agents.DeleteAgent(existingJokerAgent.Name);
|
||||||
|
|||||||
@@ -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:
|
Before you begin, ensure you have the following prerequisites:
|
||||||
|
|
||||||
- .NET 10 SDK or later
|
- .NET 10 SDK or later
|
||||||
- Microsoft Foundry service endpoint and deployment configured
|
- Azure Foundry service endpoint and deployment configured
|
||||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
- 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).
|
**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).
|
||||||
|
|
||||||
Set the following environment variables:
|
Set the following environment variables:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$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_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_MODEL_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
$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.
|
// 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 Microsoft Foundry.
|
// 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 Microsoft Foundry resource.
|
// 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.
|
||||||
// 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.
|
// 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;
|
using System.ClientModel;
|
||||||
@@ -15,7 +15,7 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? th
|
|||||||
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||||
var model = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "Phi-4-mini-instruct";
|
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 Microsoft Foundry.
|
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Azure Foundry.
|
||||||
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
|
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
|
||||||
|
|
||||||
// Create the OpenAI client with either an API key or Azure CLI credential.
|
// Create the OpenAI client with either an API key or Azure CLI credential.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Microsoft Foundry.
|
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 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.
|
||||||
|
|
||||||
**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.
|
**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:
|
Before you begin, ensure you have the following prerequisites:
|
||||||
|
|
||||||
- .NET 10 SDK or later
|
- .NET 10 SDK or later
|
||||||
- Microsoft Foundry resource
|
- Azure AI Foundry resource
|
||||||
- A model deployment in your Microsoft Foundry resource. This example defaults to using the `Phi-4-mini-instruct` model,
|
- A model deployment in your Azure AI 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
|
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.
|
variable to the name of your deployed model.
|
||||||
- An API key or role based authentication to access the Microsoft Foundry resource
|
- An API key or role based authentication to access the Azure AI 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
|
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:
|
Set the following environment variables:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Replace with your Microsoft Foundry resource endpoint
|
# 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 Microsoft Foundry models.
|
# 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.
|
||||||
$env:AZURE_OPENAI_ENDPOINT="https://ai-foundry-<myresourcename>.services.ai.azure.com/openai/v1/"
|
$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
|
# Optional, defaults to using Azure CLI for authentication if not provided
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
// This sample shows how to create and use a simple AI agent with OpenAI Assistants as the backend.
|
||||||
|
|
||||||
|
// WARNING: The Assistants API is deprecated and will be shut down.
|
||||||
|
// For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete - OpenAI Assistants API is deprecated but still used in this sample
|
||||||
|
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using OpenAI;
|
||||||
|
using OpenAI.Assistants;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
const string JokerName = "Joker";
|
||||||
|
const string JokerInstructions = "You are good at telling jokes.";
|
||||||
|
|
||||||
|
// Get a client to create/retrieve server side agents with.
|
||||||
|
var assistantClient = new OpenAIClient(apiKey).GetAssistantClient();
|
||||||
|
|
||||||
|
// You can create a server side assistant with the OpenAI SDK.
|
||||||
|
var createResult = await assistantClient.CreateAssistantAsync(model, new() { Name = JokerName, Instructions = JokerInstructions });
|
||||||
|
|
||||||
|
// You can retrieve an already created server side assistant as an AIAgent.
|
||||||
|
AIAgent agent1 = await assistantClient.GetAIAgentAsync(createResult.Value.Id);
|
||||||
|
|
||||||
|
// You can also create a server side assistant and return it as an AIAgent directly.
|
||||||
|
AIAgent agent2 = await assistantClient.CreateAIAgentAsync(
|
||||||
|
model: model,
|
||||||
|
name: JokerName,
|
||||||
|
instructions: JokerInstructions);
|
||||||
|
|
||||||
|
// You can invoke the agent like any other AIAgent.
|
||||||
|
AgentSession session = await agent1.CreateSessionAsync();
|
||||||
|
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
|
||||||
|
|
||||||
|
// Cleanup for sample purposes.
|
||||||
|
await assistantClient.DeleteAssistantAsync(agent1.Id);
|
||||||
|
await assistantClient.DeleteAssistantAsync(agent2.Id);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Prerequisites
|
||||||
|
|
||||||
|
WARNING: The Assistants API is deprecated and will be shut down.
|
||||||
|
For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration
|
||||||
|
|
||||||
|
Before you begin, ensure you have the following prerequisites:
|
||||||
|
|
||||||
|
- .NET 10 SDK or later
|
||||||
|
- OpenAI API key
|
||||||
|
|
||||||
|
Set the following environment variables:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:OPENAI_API_KEY="*****" # Replace with your OpenAI API key
|
||||||
|
$env:OPENAI_CHAT_MODEL_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
|
||||||
|
```
|
||||||
@@ -18,13 +18,14 @@ 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 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.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 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 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 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 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 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 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|
|
|[Creating an AIAgent with a custom implementation](./Agent_With_CustomImplementation/)|This sample demonstrates how to create an AIAgent with a custom implementation|
|
||||||
|[Creating an AIAgent with GitHub Copilot](./Agent_With_GitHubCopilot/)|This sample demonstrates how to create an AIAgent using GitHub Copilot SDK as the underlying inference service|
|
|[Creating an AIAgent with GitHub Copilot](./Agent_With_GitHubCopilot/)|This sample demonstrates how to create an AIAgent using GitHub Copilot SDK as the underlying inference service|
|
||||||
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|
||||||
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|
||||||
|
|[Creating an AIAgent with OpenAI Assistants](./Agent_With_OpenAIAssistants/)|This sample demonstrates how to create an AIAgent using OpenAI Assistants as the underlying inference service.</br>WARNING: The Assistants API is deprecated and will be shut down. For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration|
|
||||||
|[Creating an AIAgent with OpenAI ChatCompletion](./Agent_With_OpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using OpenAI ChatCompletion as the underlying inference service|
|
|[Creating an AIAgent with OpenAI ChatCompletion](./Agent_With_OpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using OpenAI ChatCompletion as the underlying inference service|
|
||||||
|[Creating an AIAgent with OpenAI Responses](./Agent_With_OpenAIResponses/)|This sample demonstrates how to create an AIAgent using OpenAI Responses as the underlying inference service|
|
|[Creating an AIAgent with OpenAI Responses](./Agent_With_OpenAIResponses/)|This sample demonstrates how to create an AIAgent using OpenAI Responses as the underlying inference service|
|
||||||
|
|
||||||
|
|||||||
-4
@@ -14,10 +14,6 @@
|
|||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// 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");
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# 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
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
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
@@ -0,0 +1,5 @@
|
|||||||
|
# Expense Report Template
|
||||||
|
|
||||||
|
| Date | Category | Vendor | Description | Amount (USD) | Original Currency | Original Amount | Attendees | Business Purpose | Receipt Attached |
|
||||||
|
|------|----------|--------|-------------|--------------|-------------------|-----------------|-----------|------------------|------------------|
|
||||||
|
| | | | | | | | | | Yes or No |
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# 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.
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// 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}");
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# 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
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
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
@@ -1,10 +0,0 @@
|
|||||||
# 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
@@ -1,29 +0,0 @@
|
|||||||
# 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()
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
// 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}");
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# 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,24 +1,7 @@
|
|||||||
# AgentSkills Samples
|
# AgentSkills Samples
|
||||||
|
|
||||||
Samples demonstrating Agent Skills capabilities. Each sample shows a different way to define and use skills.
|
Samples demonstrating Agent Skills capabilities.
|
||||||
|
|
||||||
| Sample | Description |
|
| Sample | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
|
| [Agent_Step01_BasicSkills](Agent_Step01_BasicSkills/) | Using Agent Skills with a ChatClientAgent, including progressive disclosure and skill resources |
|
||||||
| [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`.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
// 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('-');
|
|
||||||
}
|
|
||||||
+10
-3
@@ -5,13 +5,20 @@
|
|||||||
using Anthropic;
|
using Anthropic;
|
||||||
using Anthropic.Core;
|
using Anthropic.Core;
|
||||||
using Microsoft.Agents.AI;
|
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 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";
|
var model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
|
||||||
|
|
||||||
AIAgent agent =
|
AIAgent agent = new AnthropicClient(new ClientOptions { ApiKey = apiKey })
|
||||||
new AnthropicClient(new ClientOptions { ApiKey = apiKey })
|
|
||||||
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
|
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
|
||||||
|
|
||||||
// Invoke the agent and output the text result.
|
// Invoke the agent and output the text result.
|
||||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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/).
|
**Note**: These samples use Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||||
|
|
||||||
## Using Anthropic with Microsoft Foundry
|
## Using Anthropic with Azure Foundry
|
||||||
|
|
||||||
To use Anthropic with Microsoft Foundry, you can check the sample [AgentProviders/Agent_With_Anthropic](../AgentProviders/Agent_With_Anthropic/README.md) for more details.
|
To use Anthropic with Azure Foundry, you can check the sample [AgentProviders/Agent_With_Anthropic](../AgentProviders/Agent_With_Anthropic/README.md) for more details.
|
||||||
|
|
||||||
## Samples
|
## Samples
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.FoundryMemory\Microsoft.Agents.AI.FoundryMemory.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+7
-14
@@ -1,17 +1,17 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
|
// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
|
||||||
// The sample stores conversation messages in a Microsoft Foundry memory store and retrieves relevant
|
// The sample stores conversation messages in an Azure AI Foundry memory store and retrieves relevant
|
||||||
// memories for subsequent invocations, even across new sessions.
|
// memories for subsequent invocations, even across new sessions.
|
||||||
//
|
//
|
||||||
// Note: Memory extraction in Microsoft Foundry is asynchronous and takes time. This sample demonstrates
|
// Note: Memory extraction in Azure AI Foundry is asynchronous and takes time. This sample demonstrates
|
||||||
// a simple polling approach to wait for memory updates to complete before querying.
|
// a simple polling approach to wait for memory updates to complete before querying.
|
||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Azure.AI.Projects;
|
using Azure.AI.Projects;
|
||||||
using Azure.Identity;
|
using Azure.Identity;
|
||||||
using Microsoft.Agents.AI;
|
using Microsoft.Agents.AI;
|
||||||
using Microsoft.Agents.AI.Foundry;
|
using Microsoft.Agents.AI.FoundryMemory;
|
||||||
|
|
||||||
string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||||
string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "memory-store-sample";
|
string memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "memory-store-sample";
|
||||||
@@ -19,9 +19,6 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLO
|
|||||||
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
|
string embeddingModelName = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
|
||||||
|
|
||||||
// Create an AIProjectClient for Foundry with Azure Identity authentication.
|
// 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();
|
DefaultAzureCredential credential = new();
|
||||||
AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
|
AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
|
||||||
|
|
||||||
@@ -36,15 +33,11 @@ FoundryMemoryProvider memoryProvider = new(
|
|||||||
memoryStoreName,
|
memoryStoreName,
|
||||||
stateInitializer: _ => new(new FoundryMemoryProviderScope("sample-user-123")));
|
stateInitializer: _ => new(new FoundryMemoryProviderScope("sample-user-123")));
|
||||||
|
|
||||||
ChatClientAgent agent = projectClient.AsAIAgent(
|
AIAgent agent = await projectClient.CreateAIAgentAsync(deploymentName,
|
||||||
new ChatClientAgentOptions()
|
options: new ChatClientAgentOptions()
|
||||||
{
|
{
|
||||||
Name = "TravelAssistantWithFoundryMemory",
|
Name = "TravelAssistantWithFoundryMemory",
|
||||||
ChatOptions = new()
|
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
|
||||||
{
|
|
||||||
ModelId = deploymentName,
|
|
||||||
Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details."
|
|
||||||
},
|
|
||||||
AIContextProviders = [memoryProvider]
|
AIContextProviders = [memoryProvider]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +54,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("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));
|
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
|
||||||
|
|
||||||
// Memory extraction in Microsoft Foundry is asynchronous and takes time to process.
|
// Memory extraction in Azure AI Foundry is asynchronous and takes time to process.
|
||||||
// WhenUpdatesCompletedAsync polls all pending updates and waits for them to complete.
|
// WhenUpdatesCompletedAsync polls all pending updates and waits for them to complete.
|
||||||
Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
|
Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
|
||||||
await memoryProvider.WhenUpdatesCompletedAsync();
|
await memoryProvider.WhenUpdatesCompletedAsync();
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
# Agent with Memory Using Microsoft Foundry
|
# Agent with Memory Using Azure AI Foundry
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## Features Demonstrated
|
## Features Demonstrated
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ This sample demonstrates how to create and run an agent that uses Microsoft Foun
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. Azure subscription with Microsoft Foundry project
|
1. Azure subscription with Azure AI 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)
|
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
|
3. .NET 10.0 SDK
|
||||||
4. Azure CLI logged in (`az login`)
|
4. Azure CLI logged in (`az login`)
|
||||||
@@ -21,7 +21,7 @@ This sample demonstrates how to create and run an agent that uses Microsoft Foun
|
|||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Microsoft Foundry project endpoint and memory store name
|
# Azure AI 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_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
|
||||||
export AZURE_AI_MEMORY_STORE_ID="my_memory_store"
|
export AZURE_AI_MEMORY_STORE_ID="my_memory_store"
|
||||||
|
|
||||||
@@ -48,10 +48,10 @@ The agent will:
|
|||||||
|
|
||||||
## Key Differences from Mem0
|
## Key Differences from Mem0
|
||||||
|
|
||||||
| Aspect | Mem0 | Microsoft Foundry Memory |
|
| Aspect | Mem0 | Azure AI Foundry Memory |
|
||||||
|--------|------|------------------------|
|
|--------|------|------------------------|
|
||||||
| Authentication | API Key | Azure Identity (DefaultAzureCredential) |
|
| Authentication | API Key | Azure Identity (DefaultAzureCredential) |
|
||||||
| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
|
| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
|
||||||
| Memory Types | Single memory store | User Profile + Chat Summary |
|
| Memory Types | Single memory store | User Profile + Chat Summary |
|
||||||
| Hosting | Mem0 cloud or self-hosted | Microsoft Foundry managed service |
|
| Hosting | Mem0 cloud or self-hosted | Azure AI Foundry managed service |
|
||||||
| Store Creation | N/A (automatic) | Explicit via `EnsureMemoryStoreCreatedAsync` |
|
| 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.
|
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.|
|
|[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.|
|
|[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.|
|
|[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 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.|
|
|[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.|
|
||||||
|[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.|
|
|[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](../AgentsWithFoundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
|
> **See also**: [Memory Search with Foundry Agents](../FoundryAgents/FoundryAgents_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Azure Foundry Agents.
|
||||||
|
|||||||
@@ -4,14 +4,28 @@
|
|||||||
|
|
||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
using Microsoft.Agents.AI;
|
using Microsoft.Agents.AI;
|
||||||
using OpenAI.Responses;
|
using OpenAI;
|
||||||
|
using OpenAI.Chat;
|
||||||
|
|
||||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
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";
|
var model = Environment.GetEnvironmentVariable("OPENAI_CHAT_MODEL_NAME") ?? "gpt-4o-mini";
|
||||||
|
|
||||||
AIAgent agent =
|
AIAgent agent = new OpenAIClient(apiKey)
|
||||||
new ResponsesClient(new ApiKeyCredential(apiKey))
|
.GetChatClient(model)
|
||||||
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
|
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||||
|
|
||||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
|
||||||
Console.WriteLine(await agent.RunAsync("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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+4
-16
@@ -73,28 +73,16 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
|
|||||||
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
|
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
|
||||||
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
|
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 messageId = element.GetProperty("id"u8).ToString();
|
||||||
string messageRole = roleElement.ToString();
|
string messageRole = element.GetProperty("role"u8).ToString();
|
||||||
Console.WriteLine($" Message ID: {messageId}");
|
Console.WriteLine($" Message ID: {messageId}");
|
||||||
Console.WriteLine($" Message Role: {messageRole}");
|
Console.WriteLine($" Message Role: {messageRole}");
|
||||||
|
|
||||||
if (element.TryGetProperty("content"u8, out var contentElement))
|
foreach (var content in element.GetProperty("content").EnumerateArray())
|
||||||
{
|
{
|
||||||
foreach (var content in contentElement.EnumerateArray())
|
string messageContentText = content.GetProperty("text"u8).ToString();
|
||||||
{
|
Console.WriteLine($" Message Text: {messageContentText}");
|
||||||
if (content.TryGetProperty("text"u8, out var textElement))
|
|
||||||
{
|
|
||||||
Console.WriteLine($" Message Text: {textElement}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
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 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 deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
||||||
var afOverviewUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/overview/index.md";
|
var afOverviewUrl = "https://github.com/MicrosoftDocs/semantic-kernel-docs/blob/main/agent-framework/overview/agent-framework-overview.md";
|
||||||
var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/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.
|
// 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.
|
- 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.
|
- 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 Microsoft 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 Azure AI 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).
|
**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).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+9
-17
@@ -4,13 +4,11 @@
|
|||||||
|
|
||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
using Azure.AI.Projects;
|
using Azure.AI.Projects;
|
||||||
using Azure.AI.Projects.Agents;
|
|
||||||
using Azure.Identity;
|
using Azure.Identity;
|
||||||
using Microsoft.Agents.AI;
|
using Microsoft.Agents.AI;
|
||||||
using Microsoft.Agents.AI.Foundry;
|
using Microsoft.Extensions.AI;
|
||||||
using OpenAI;
|
using OpenAI;
|
||||||
using OpenAI.Files;
|
using OpenAI.Files;
|
||||||
using OpenAI.Responses;
|
|
||||||
using OpenAI.VectorStores;
|
using OpenAI.VectorStores;
|
||||||
|
|
||||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||||
@@ -39,20 +37,14 @@ ClientResult<VectorStore> vectorStoreCreate = await vectorStoreClient.CreateVect
|
|||||||
FileIds = { uploadResult.Value.Id }
|
FileIds = { uploadResult.Value.Id }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the native OpenAI SDK FileSearchTool directly with the vector store ID.
|
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreCreate.Value.Id)] };
|
||||||
#pragma warning disable OPENAI001
|
|
||||||
FileSearchTool fileSearchTool = new([vectorStoreCreate.Value.Id]);
|
|
||||||
#pragma warning restore OPENAI001
|
|
||||||
|
|
||||||
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
AIAgent agent = await aiProjectClient
|
||||||
"AskContoso",
|
.CreateAIAgentAsync(
|
||||||
new ProjectsAgentVersionCreationOptions(
|
model: deploymentName,
|
||||||
new DeclarativeAgentDefinition(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.",
|
||||||
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]);
|
||||||
Tools = { fileSearchTool }
|
|
||||||
}));
|
|
||||||
FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
|
|
||||||
|
|
||||||
AgentSession session = await agent.CreateSessionAsync();
|
AgentSession session = await agent.CreateSessionAsync();
|
||||||
|
|
||||||
@@ -68,4 +60,4 @@ Console.WriteLine(await agent.RunAsync("What is the best way to maintain the Tra
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
await fileClient.DeleteFileAsync(uploadResult.Value.Id);
|
await fileClient.DeleteFileAsync(uploadResult.Value.Id);
|
||||||
await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreCreate.Value.Id);
|
await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreCreate.Value.Id);
|
||||||
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
|
||||||
|
|||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user