Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db4b4c2736 | |||
| ad654b523a | |||
| 172c0a9507 |
@@ -42,7 +42,7 @@ jobs:
|
||||
coreChanged: ${{ steps.filter.outputs.core }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -610,12 +610,12 @@ jobs:
|
||||
python-version: "3.13"
|
||||
os: ${{ runner.os }}
|
||||
- name: Download all test results from current run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: dotnet-test-results-*
|
||||
path: dotnet-test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/dotnet-integration-report-history.json
|
||||
key: dotnet-integration-report-history-${{ github.run_id }}
|
||||
@@ -632,7 +632,7 @@ jobs:
|
||||
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/dotnet-integration-report-history.json
|
||||
key: dotnet-integration-report-history-${{ github.run_id }}
|
||||
|
||||
@@ -54,14 +54,11 @@ jobs:
|
||||
- name: Find csproj files
|
||||
id: find-csproj
|
||||
if: github.event_name != 'pull_request' || steps.changed-files.outputs.added_modified != '' || steps.changed-files.outcome == 'failure'
|
||||
env:
|
||||
ADDED_MODIFIED: ${{ steps.changed-files.outputs.added_modified }}
|
||||
run: |
|
||||
csproj_files=()
|
||||
exclude_files=("Experimental.Orchestration.Flow.csproj" "Experimental.Orchestration.Flow.UnitTests.csproj" "Experimental.Orchestration.Flow.IntegrationTests.csproj")
|
||||
set -f
|
||||
if [[ ${{ steps.changed-files.outcome }} == 'success' ]]; then
|
||||
for file in $ADDED_MODIFIED; do
|
||||
for file in ${{ steps.changed-files.outputs.added_modified }}; do
|
||||
echo "$file was changed"
|
||||
dir="./$file"
|
||||
while [[ $dir != "." && $dir != "/" && $dir != $GITHUB_WORKSPACE ]]; do
|
||||
@@ -83,7 +80,6 @@ jobs:
|
||||
csproj_files=($(printf "%s\n" "${csproj_files[@]}" | sort -u))
|
||||
echo "Found ${#csproj_files[@]} unique csproj/slnx files: ${csproj_files[*]}"
|
||||
echo "csproj_files=${csproj_files[*]}" >> $GITHUB_OUTPUT
|
||||
set +f
|
||||
|
||||
- name: Pull container dotnet/sdk:${{ matrix.dotnet }}
|
||||
if: steps.find-csproj.outputs.csproj_files != ''
|
||||
@@ -92,11 +88,8 @@ jobs:
|
||||
# This step will run dotnet format on each of the unique csproj files and fail if any changes are made
|
||||
- name: Run dotnet format
|
||||
if: steps.find-csproj.outputs.csproj_files != ''
|
||||
env:
|
||||
CSPROJ_FILES: ${{ steps.find-csproj.outputs.csproj_files }}
|
||||
run: |
|
||||
set -f
|
||||
for csproj in $CSPROJ_FILES; do
|
||||
for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
|
||||
echo "Running dotnet format on $csproj"
|
||||
docker run --rm -v "$(pwd):/app" -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} dotnet format "$csproj" --verify-no-changes --verbosity diagnostic
|
||||
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic"
|
||||
done
|
||||
|
||||
@@ -109,9 +109,6 @@ jobs:
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
|
||||
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
|
||||
# Foundry
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
|
||||
|
||||
- name: Write Job Summary
|
||||
if: always()
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
with:
|
||||
path: ~/.cache/prek
|
||||
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
- uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1
|
||||
name: Run Pre-commit Hooks (excluding poe-check)
|
||||
env:
|
||||
SKIP: poe-check
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: python-dependency-maintenance
|
||||
@@ -319,113 +320,46 @@ jobs:
|
||||
git push --force-with-lease --set-upstream origin "${BRANCH}"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create or update dependency maintenance tracking issue
|
||||
- name: Create or update pull request with GitHub CLI
|
||||
if: steps.commit_updates.outputs.has_changes == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const branch = "automation/python-dependency-maintenance"
|
||||
const prTitle = "Python: chore: update dependencies"
|
||||
const issueTitle = "Python dependency maintenance PR needed"
|
||||
const owner = context.repo.owner
|
||||
const repo = context.repo.repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH="automation/python-dependency-maintenance"
|
||||
PR_TITLE="Python: chore: update dependencies"
|
||||
PR_BODY_FILE="$(mktemp)"
|
||||
|
||||
const branchRef = await github.rest.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `heads/${branch}`,
|
||||
})
|
||||
const branchSha = branchRef.data.object.sha
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`
|
||||
const compareUrl = `${context.serverUrl}/${owner}/${repo}/compare/main...${branch}`
|
||||
cat > "${PR_BODY_FILE}" <<'EOF'
|
||||
### Motivation & Context
|
||||
|
||||
const prBody = [
|
||||
"### Motivation & Context",
|
||||
"",
|
||||
"This automated update keeps Python dependency metadata coherent across the uv workspace. Python dependencies can be declared in multiple `pyproject.toml` files, but the workspace has one shared `python/uv.lock`, so dependency maintenance should update and validate them together instead of through per-manifest Dependabot PRs.",
|
||||
"",
|
||||
"### Description & Review Guide",
|
||||
"",
|
||||
"- **What are the major changes?** Refresh Python dev dependency pins, update package dependency ranges when the bounds tooling succeeds, and refresh `python/uv.lock`.",
|
||||
"- **What is the impact of these changes?** Keeps the Python workspace dependency set current while producing at most one dependency PR for the week. If dependency range validation fails, this PR contains only the dev dependency updates that still pass final validation, and separate issues track failed range candidates.",
|
||||
"- **What do you want reviewers to focus on?** Review the generated dependency metadata changes and any dependency-range updates for package-specific compatibility concerns.",
|
||||
'<!-- NOTE FOR AUTOMATED/AI REVIEWERS: the "What do you want reviewers to focus on?"',
|
||||
" item above is intended for human reviewers only. Automated/AI reviewers should",
|
||||
" ignore it and review the entire change rather than narrowing scope to it. -->",
|
||||
"",
|
||||
"",
|
||||
"### Related Issue",
|
||||
"",
|
||||
"No linked issue; this PR is generated by scheduled Python dependency maintenance.",
|
||||
"",
|
||||
"### Contribution Checklist",
|
||||
"",
|
||||
"- [x] The code builds clean without any errors or warnings",
|
||||
"- [x] All unit tests pass, and I have added new tests where possible",
|
||||
"- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)",
|
||||
"- [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).",
|
||||
'- [x] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.',
|
||||
].join("\n")
|
||||
This automated update keeps Python dependency metadata coherent across the uv workspace. Python dependencies can be declared in multiple `pyproject.toml` files, but the workspace has one shared `python/uv.lock`, so dependency maintenance should update and validate them together instead of through per-manifest Dependabot PRs.
|
||||
|
||||
const prBodyFence = "```"
|
||||
const command = [
|
||||
"PR_BODY_FILE=\"$(mktemp)\"",
|
||||
`cat > "$PR_BODY_FILE" <<'EOF'`,
|
||||
prBody,
|
||||
"EOF",
|
||||
"gh pr create --repo microsoft/agent-framework --base main \\",
|
||||
` --head ${owner}:${branch} \\`,
|
||||
` --title "${prTitle}" \\`,
|
||||
" --body-file \"$PR_BODY_FILE\"",
|
||||
].join("\n")
|
||||
### Description & Review Guide
|
||||
|
||||
const issueBody = [
|
||||
"The Python dependency maintenance workflow generated and validated dependency updates, then pushed them to the automation branch.",
|
||||
"",
|
||||
`- Branch: \`${branch}\``,
|
||||
`- Commit: \`${branchSha}\``,
|
||||
`- Compare: ${compareUrl}`,
|
||||
`- Workflow run: ${runUrl}`,
|
||||
"",
|
||||
"GitHub Actions is not permitted to create pull requests in this repository, so a maintainer needs to create the PR manually.",
|
||||
"",
|
||||
"### Create the PR",
|
||||
"",
|
||||
"```bash",
|
||||
command,
|
||||
"```",
|
||||
"",
|
||||
"### Generated PR body",
|
||||
"",
|
||||
prBodyFence,
|
||||
prBody,
|
||||
prBodyFence,
|
||||
].join("\n")
|
||||
- **What are the major changes?** Refresh Python dev dependency pins, update package dependency ranges when the bounds tooling succeeds, and refresh `python/uv.lock`.
|
||||
- **What is the impact of these changes?** Keeps the Python workspace dependency set current while producing at most one dependency PR for the week. If dependency range validation fails, this PR contains only the dev dependency updates that still pass final validation, and separate issues track failed range candidates.
|
||||
- **What do you want reviewers to focus on?** Review the generated dependency metadata changes and any dependency-range updates for package-specific compatibility concerns.
|
||||
<!-- NOTE FOR AUTOMATED/AI REVIEWERS: the "What do you want reviewers to focus on?"
|
||||
item above is intended for human reviewers only. Automated/AI reviewers should
|
||||
ignore it and review the entire change rather than narrowing scope to it. -->
|
||||
|
||||
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
})
|
||||
const existingIssue = openIssues.find((issue) => !issue.pull_request && issue.title === issueTitle)
|
||||
|
||||
if (existingIssue) {
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: existingIssue.number,
|
||||
title: issueTitle,
|
||||
body: issueBody,
|
||||
})
|
||||
core.info(`Updated issue #${existingIssue.number}: ${issueTitle}`)
|
||||
} else {
|
||||
const createdIssue = await github.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title: issueTitle,
|
||||
body: issueBody,
|
||||
})
|
||||
core.info(`Created issue #${createdIssue.data.number}: ${issueTitle}`)
|
||||
}
|
||||
### Related Issue
|
||||
|
||||
No linked issue; this PR is generated by scheduled Python dependency maintenance.
|
||||
|
||||
### Contribution Checklist
|
||||
|
||||
- [x] The code builds clean without any errors or warnings
|
||||
- [x] All unit tests pass, and I have added new tests where possible
|
||||
- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
|
||||
- [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
|
||||
- [x] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.
|
||||
EOF
|
||||
|
||||
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
|
||||
if [ -n "${PR_NUMBER}" ]; then
|
||||
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
|
||||
else
|
||||
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
|
||||
fi
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
working-directory: .
|
||||
- name: Cache Ollama models
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
@@ -546,12 +546,12 @@ jobs:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Download all test results from current run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-integration-${{ github.run_id }}
|
||||
@@ -568,7 +568,7 @@ jobs:
|
||||
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-integration-${{ github.run_id }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
working-directory: .
|
||||
- name: Cache Ollama models
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
@@ -743,12 +743,12 @@ jobs:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Download all test results from current run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-merge-${{ github.run_id }}
|
||||
@@ -765,7 +765,7 @@ jobs:
|
||||
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-merge-${{ github.run_id }}
|
||||
|
||||
@@ -693,7 +693,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Download all validation reports
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: validation-report-*
|
||||
path: reports/
|
||||
@@ -701,7 +701,7 @@ jobs:
|
||||
|
||||
- name: Restore validation history
|
||||
id: cache-restore
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
@@ -719,7 +719,7 @@ jobs:
|
||||
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save validation history
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
echo "PR_NUMBER=$ARTIFACT_PR_NUMBER" >> "$GITHUB_ENV"
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@dd5b80bde6d16941f336518e92929e89069d8451 # v1.7.2
|
||||
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[](https://learn.microsoft.com/en-us/agent-framework/)
|
||||
[](https://pypi.org/project/agent-framework/)
|
||||
[](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
|
||||
[](https://github.com/microsoft/agent-framework)
|
||||
[](https://github.com/microsoft/agent-framework/stargazers)
|
||||
|
||||
|
||||
Microsoft Agent Framework (MAF) is an open, multi-language framework for building **production-grade AI agents and multi-agent workflows** in **.NET and Python**.
|
||||
|
||||
@@ -11,7 +11,7 @@ trigger:
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_student
|
||||
conversationId: =System.ConversationId
|
||||
|
||||
@@ -43,11 +43,6 @@ FIDES (Flow Integrity Deterministic Enforcement System) is a label-based securit
|
||||
3. **Variable Indirection** — `ContentVariableStore` and `VariableReferenceContent` for physical isolation of untrusted content from the LLM context.
|
||||
4. **Quarantined Execution** — `quarantined_llm` and `inspect_variable` tools for isolated processing of untrusted data with audit logging.
|
||||
|
||||
In addition, remote MCP integrations are secured through two mechanisms:
|
||||
|
||||
- **Hint-based tool auto-labeling**: MCP `ToolAnnotations` (`readOnlyHint`, `openWorldHint`, etc.) are mapped to FIDES tool properties (`source_integrity`, `accepts_untrusted`, `max_allowed_confidentiality`).
|
||||
- **Server `_meta.ifc` result labels**: MCP result metadata is parsed into per-item `security_label` values, so provider-supplied IFC labels are enforced by middleware.
|
||||
|
||||
### Consequences
|
||||
|
||||
- Good, because it provides deterministic security guarantees about what untrusted content can influence.
|
||||
@@ -122,13 +117,6 @@ Monitor agent behavior and block suspicious actions post-facto.
|
||||
- Uses existing `FunctionMiddleware` base class.
|
||||
- Attaches labels via `additional_properties` (no schema changes).
|
||||
- Leverages `SerializationMixin` for label persistence.
|
||||
- Integrates MCP hint/result metadata through `additional_properties` keys (`max_allowed_confidentiality`, `source_integrity`, `__mcp_result_meta__`) without transport-specific policy code in core middleware.
|
||||
|
||||
### MCP-Specific Security Notes
|
||||
|
||||
- `SecureMCPToolProxy` applies `apply_mcp_security_labels(...)` automatically when connecting an MCP tool or URL.
|
||||
- For servers like the GitHub MCP server (with `X-MCP-Features: ifc_labels`), `_meta.ifc` labels are considered authoritative for per-result label assignment.
|
||||
- Tools that are not explicitly `readOnlyHint=True` are treated as potential sinks and default to `max_allowed_confidentiality=PUBLIC` to prevent exfiltration.
|
||||
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md)
|
||||
status: accepted
|
||||
contact: rogerbarreto
|
||||
date: 2026-06-29
|
||||
date: 2026-05-07
|
||||
deciders: rogerbarreto
|
||||
consulted: []
|
||||
informed: []
|
||||
@@ -9,8 +9,6 @@ informed: []
|
||||
|
||||
# Hosted session identity context for Foundry Hosting
|
||||
|
||||
> **Superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).** `Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) replaced `ResponseContext.Isolation` (`UserIsolationKey` / `ChatIsolationKey`, headers `x-agent-user-isolation-key` / `x-agent-chat-isolation-key`) with `ResponseContext.PlatformContext` (`UserIdKey` / `CallId`, headers `x-agent-user-id` / `x-agent-foundry-call-id`). The chat isolation key was removed and `HostedSessionContext` is now user-only. This ADR is retained as the historical record of the original design.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Server-hosted Foundry agents need a way to scope per-user state (most notably `FoundryMemoryProvider` memories) by the end user that initiated the request. The Foundry platform already injects `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers on every Responses request, but the agent-framework hosting layer did not surface those values to `AIContextProvider` instances. The provider's `stateInitializer` only received an `AgentSession?` with no identity attached, so per-user scoping was impossible without out-of-band plumbing.
|
||||
|
||||
@@ -1,518 +1,144 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-06-30
|
||||
date: 2026-06-11
|
||||
deciders: eavanvalkenburg
|
||||
consulted: rogerbarreto, moonbox3
|
||||
---
|
||||
|
||||
# Python protocol helpers and optional execution state
|
||||
# Python minimal hosting core and pluggable channels
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Agent Framework needs to help applications expose agents and workflows over external protocols such as OpenAI
|
||||
Responses, Telegram, Activity Protocol, and future transports.
|
||||
Agent Framework has several protocol-specific hosting surfaces. App authors who want one agent or workflow on multiple protocols must compose servers, routes, middleware, session handling, and lifecycle code by hand.
|
||||
|
||||
FastAPI, Starlette, Azure Functions, Django, Telegram SDKs, Bot Framework SDKs, and other app frameworks already own
|
||||
route registration, dependency injection, middleware, authentication, background tasks, lifecycle, and native client
|
||||
calls. Agent Framework should not duplicate those surfaces unless a specific hosting environment requires it.
|
||||
We will introduce a small Python hosting core that owns the common server shape and leaves protocol details inside channel packages. The first public contract must be intentionally narrow so Python can ship a base contract before adding identity linking, proactive delivery, or multicast behavior. Other language implementations may reuse the same conceptual boundary, but this ADR records the Python decision.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Keep the released surface small enough to explain without first teaching a channel framework.
|
||||
- Provide reusable Agent Framework run translation that works with FastAPI, Django, and other web frameworks.
|
||||
- Let app/framework code own route declaration, auth, middleware, native SDK clients, command handling, and background
|
||||
work.
|
||||
- Keep stateful execution support explicit: session lookup/storage and workflow checkpoint lookup/storage may still need
|
||||
a small AF-owned home.
|
||||
- Keep the first host easy to explain: one app, one hostable target, one or more channels.
|
||||
- Reuse Agent Framework's existing agent, workflow, session, history, and checkpoint primitives.
|
||||
- Let channel packages own protocol parsing, protocol responses, authentication details, and native command surfaces.
|
||||
- Make session continuity explicit through a channel-supplied `ChannelSession(isolation_key=...)`.
|
||||
- Avoid approving cross-channel identity and delivery semantics before their safety model is reviewed.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. Create protocol-specific hosts.
|
||||
2. Ship a full host/channel framework with route contribution and channel hooks.
|
||||
3. Ship protocol conversion helpers plus optional execution state.
|
||||
1. Keep only protocol-specific hosts.
|
||||
2. Ship a large hosting core with identity linking, authorization, background delivery, active-channel routing, and multicast in v1.
|
||||
3. Ship a minimal host/channel core now and track linking/multicast as follow-up work.
|
||||
|
||||
### 1. Create protocol-specific hosts
|
||||
### Keep only protocol-specific hosts
|
||||
|
||||
- Good: no new shared abstraction.
|
||||
- Neutral: each protocol host can evolve independently.
|
||||
- Bad: every package reinvents AF input/result mapping, session-key conventions, and stateful execution helpers.
|
||||
- Good: no new abstraction or package surface.
|
||||
- Neutral: each protocol can continue evolving independently.
|
||||
- Bad: every multi-channel app still has to compose servers, lifecycle, and session handling by hand.
|
||||
|
||||
### 2. Ship a full host/channel framework
|
||||
### Ship the large cross-channel host in v1
|
||||
|
||||
- Good: one object can assemble routes, channels, session handling, hooks, and lifecycle callbacks.
|
||||
- Good: app code using the supported host shape can be short.
|
||||
- Bad: the framework owns concerns already handled by web frameworks, protocol SDKs and/or other services.
|
||||
- Bad: users must understand `Channel`, contribution, hook, and host-dispatch concepts before they can see how a request
|
||||
becomes `agent.run(...)`.
|
||||
- Bad: the abstraction is hard to reuse outside the chosen web framework.
|
||||
- Good: the richest cross-channel scenarios are available immediately.
|
||||
- Neutral: the host becomes the natural place to demonstrate identity and delivery policy.
|
||||
- Bad: v1 becomes a security-sensitive identity and delivery system before the safety model is reviewed.
|
||||
|
||||
### 3. Ship protocol helpers plus optional execution state
|
||||
### Ship the minimal core now
|
||||
|
||||
- Good: protocol packages provide the Agent Framework run value directly: `<protocol>_to_run(...)` and
|
||||
`<protocol>_from_run(...)` style helpers.
|
||||
- Good: apps keep native FastAPI, Starlette, Azure Functions, Django, Bot Framework, or Telegram SDK code.
|
||||
- Good: helper functions can be tested without a web framework app or host pipeline.
|
||||
- Good: small state objects can still own target-coupled state: `AgentState` pairs an agent target with a `SessionStore`,
|
||||
and `WorkflowState` resolves a workflow target while reusing the existing `CheckpointStorage` abstraction.
|
||||
- Good: provides maximum configurability in handling input and outputs (outside of the conversions)
|
||||
- Bad: building a first iteration of a new Host is more verbose.
|
||||
- Bad: samples show more explicit route/client code than a fully assembled channel host.
|
||||
- Good: the host/channel boundary can be implemented, tested, and explained without solving linking and durable delivery at the same time.
|
||||
- Neutral: apps that need richer behavior must build it locally or wait for ADR-0028 follow-up work.
|
||||
- Bad: proactive delivery and multicast scenarios are deliberately absent from v1.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen option: **3. Ship protocol helpers plus optional execution state**.
|
||||
Chosen option: **minimal host/channel core now, follow-up enhancements later**.
|
||||
|
||||
Protocol packages own:
|
||||
`AgentFrameworkHost` owns:
|
||||
|
||||
- parsing protocol-native input into Agent Framework run input and options;
|
||||
- rendering `AgentResponse`, `AgentResponseUpdate`, workflow results, or workflow updates back into protocol-native
|
||||
response/event payloads;
|
||||
- protocol-specific isolation/session id helper functions when useful, such as `telegram_session_id(update)`;
|
||||
- protocol-specific typing/update event helpers where the protocol has a native concept.
|
||||
- one application object,
|
||||
- one hostable target (`SupportsAgentRun` agent-compatible object or a `Workflow`), and
|
||||
- one or more channels.
|
||||
|
||||
Application or web-framework code owns:
|
||||
Channels own:
|
||||
|
||||
- HTTP route declaration and route grouping;
|
||||
- dependency injection;
|
||||
- authentication and authorization;
|
||||
- middleware;
|
||||
- background tasks and webhook acknowledgement policy;
|
||||
- native protocol SDK clients and outbound calls;
|
||||
- command registration and command dispatch;
|
||||
- request/response status codes and framework-specific error handling;
|
||||
- choosing the isolation/session id source for the current deployment and route.
|
||||
- contributed routes, middleware, commands, and lifecycle callbacks,
|
||||
- protocol-native request parsing into `ChannelRequest`,
|
||||
- protocol-native rendering of the originating response, and
|
||||
- any channel-specific authentication or signature validation.
|
||||
|
||||
The application builder can make the server exactly as they see fit, but this is outside the responsibilities of this proposed scheme.
|
||||
This might include implementing other known API surfaces from vendors like OpenAI, such as creating conversations, vector stores, deleting things, etc.
|
||||
If they want they can build the full OpenAI API, but it will include code that does not rely on agent-framework-hosting, which is fine.
|
||||
They are responsible for what they expose.
|
||||
The host owns:
|
||||
|
||||
The optional execution-state helpers, if provided, are limited to shared execution state:
|
||||
- route/lifecycle aggregation,
|
||||
- invocation of the target,
|
||||
- `ChannelSession(isolation_key=...)` to `AgentSession` resolution and caching,
|
||||
- `reset_session(isolation_key=...)`,
|
||||
- host-level middleware, including Foundry isolation middleware only when the Foundry hosting environment flag is present,
|
||||
- invocation of per-channel hooks (`ChannelRunHook`, `ChannelResponseHook`, `ChannelStreamUpdateHook`), and
|
||||
- workflow checkpoint wiring through an explicit `checkpoint_location`.
|
||||
|
||||
- `AgentState`: one `SupportsAgentRun`-compatible target plus a `SessionStore`;
|
||||
- `WorkflowState`: one `Workflow`, `WorkflowBuilder`-shaped builder, orchestration builder, or workflow factory;
|
||||
- `SessionStore`: plain async storage (`get` / `set` / `delete`) by an app-selected id.
|
||||
`ChannelIdentity`, when present, is request metadata only. In v1 it is not a linking, authorization, or delivery key.
|
||||
|
||||
The store does not create sessions. `AgentState` provides the target-aware `get_or_create_session(...)` helper because
|
||||
only the state object has both the store and the resolved agent target. Workflow checkpointing should use the existing
|
||||
`CheckpointStorage` abstraction directly; app/state code may keep a small cursor (`session_id -> checkpoint_id`) when it
|
||||
needs to resume a workflow for a session.
|
||||
### Trust boundary for `isolation_key`
|
||||
|
||||
These objects are **not** app objects, channel registries, or route owners. They do not own FastAPI/Starlette setup,
|
||||
route contribution, protocol dispatch, command projection, or native SDK calls.
|
||||
The host treats `ChannelSession.isolation_key` as a session partition key, not as proof of identity. Channels or host middleware must authenticate and authorize any externally supplied value before passing it to the host. For example, a Responses caller must not be allowed to choose an arbitrary `previous_response_id` or header-derived key unless the platform or middleware has already established that the caller owns that conversation. The host deliberately does not infer that trust from the string itself.
|
||||
|
||||
### Helper naming and families
|
||||
### Hook ownership
|
||||
|
||||
Helpers should be protocol-specific, not generic. Avoid a generic `protocol_to_run(...)` name in public samples because it
|
||||
hides the protocol-specific contract behind a second abstraction.
|
||||
Channels provide hook configuration and protocol-native context. The host invokes those hooks as part of the common invocation pipeline:
|
||||
|
||||
Protocol packages should consider these helper families. This table is a set of examples, not a required protocol or
|
||||
checklist. Not every protocol needs every helper, but when a protocol has the concept the naming should stay consistent:
|
||||
- `ChannelRunHook` runs after channel parsing and before target invocation.
|
||||
- `ChannelResponseHook` runs after target invocation and before the originating channel serializes its response.
|
||||
- `ChannelStreamUpdateHook` is applied by the host while the channel consumes streamed updates because streaming serialization is protocol-specific.
|
||||
|
||||
| Helper family | Shape | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Run conversion | `<protocol>_to_run(...)` | Convert one protocol-native call/update/request into `Agent.run` or `Workflow.run` values. |
|
||||
| Final rendering | `<protocol>_from_run(...)` | Convert a final `AgentResponse` / workflow result into protocol-native response payloads or operations. |
|
||||
| Stream rendering | `<protocol>_from_streaming_run(...)` | Convert `ResponseStream` / workflow updates into protocol-native events or operations. |
|
||||
| Session id extraction | `<protocol>_session_id(...)` | Extract the protocol's natural continuation/partition key from the call, if present. |
|
||||
| Command/action parsing | `<protocol>_command(...)` | Parse a protocol-native command/action/operation name without deciding app policy. |
|
||||
`ChannelStreamUpdateHook` is an update hook, not a final-response sanitizer. Channels that use it for redaction or filtering must also apply equivalent policy to any final response they render. Channels choose whether the response is streaming before run hooks execute.
|
||||
|
||||
Examples:
|
||||
This keeps hook call conventions centralized while leaving protocol payload parsing and response formatting in channel packages.
|
||||
|
||||
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
|
||||
`responses_session_id(...)`;
|
||||
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
|
||||
`telegram_session_id(...)`, `telegram_command(...)`;
|
||||
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
|
||||
- `discord_to_run(...)`, `discord_from_run(...)`, `discord_session_id(...)`, `discord_command(...)`.
|
||||
### State owned by v1
|
||||
|
||||
The app still owns what a parsed command means. For example, a Telegram `/new`, Discord slash command, Bot Framework
|
||||
command activity, or A2A cancellation/request action may parse through a command/action helper, but the route or SDK
|
||||
handler decides whether that command clears a session, cancels a task, calls an agent, or is ignored.
|
||||
|
||||
Additional helper functions can be protocol-specific when the concept is not broadly shared. Examples include
|
||||
`telegram_chat_id(...)`, `telegram_callback_query_id(...)`, `telegram_media_file_id(...)`,
|
||||
`discord_interaction_id(...)`, `a2a_task_id(...)`, `a2a_context_id(...)`, and MCP tool/prompt/resource helpers. These
|
||||
helpers should still stay side-effect-free: they extract, normalize, or describe protocol data, while app/native SDK code
|
||||
performs acknowledgements, sends/edits messages, resolves protected file URLs, applies rate limits, and registers
|
||||
handlers.
|
||||
|
||||
### Security responsibilities for application builders
|
||||
|
||||
The application builder owns the trust boundary. Protocol helper packages can parse native payloads and expose candidate
|
||||
ids or operations, but they do not authenticate callers, authorize access to state, or decide which side effects are
|
||||
allowed.
|
||||
|
||||
Application code that uses these helpers are responsible for (this means that we advice you to think through these topics,
|
||||
but ultimately, the choice of which controls are needed for the intended use case is up to the application builder):
|
||||
|
||||
- authenticate the caller through the app's normal mechanism before using protocol-provided ids;
|
||||
- authorize any caller-supplied session, checkpoint, task, context, conversation, thread, or response id before loading
|
||||
state for it;
|
||||
- bind externally supplied ids to the authenticated user, tenant, workspace, installation, or chat context before using
|
||||
them as `SessionStore` keys or checkpoint cursor keys;
|
||||
- treat `<protocol>_session_id(...)` results as untrusted candidate keys until that ownership check has passed;
|
||||
- keep platform-provided isolation helpers fail-closed outside their trusted hosting environment;
|
||||
- authorize command/action effects such as reset, cancel, approve, submit, or tool invocation after parsing them;
|
||||
- opt in explicitly before resolving protected media/resource/file URLs and passing them to a remote model provider;
|
||||
- persist post-run session or checkpoint state only after `agent.run(...)`, `workflow.run(...)`, or stream finalization has
|
||||
updated that state.
|
||||
|
||||
For Foundry specifically, helpers may read values established by Foundry hosting middleware, but must not treat raw
|
||||
request headers as trusted Foundry isolation when the app is running outside Foundry. Implementations must test that
|
||||
non-Foundry requests do not accept spoofable isolation headers as platform-provided keys.
|
||||
|
||||
For workflow checkpointing, the checkpoint boundary must be at least as specific as the authorized session/tenant
|
||||
boundary. A shared storage lookup such as "latest checkpoint for workflow name" is safe only when the storage is already
|
||||
scoped to the authorized session. In a shared durable store, map the authorized `session_id` to a checkpoint id or other
|
||||
cursor and load that specific checkpoint.
|
||||
|
||||
### Session continuity
|
||||
|
||||
Session continuity remains explicit. Run parsing and isolation/session id selection are separate operations because
|
||||
isolation can come from more than one source:
|
||||
|
||||
- protocol input, such as OpenAI Responses `previous_response_id`, a Telegram chat id, or an Activity conversation id;
|
||||
- running environment, such as Foundry Hosted Agents user/chat isolation context;
|
||||
- app-specific trusted middleware or route state.
|
||||
|
||||
The app chooses which helper to call for that route and deployment. For example:
|
||||
|
||||
- `responses_session_id(body)` from `agent-framework-hosting-responses`, which can return either a `resp_*` previous
|
||||
response id or a `conv_*` conversation id when present;
|
||||
- `telegram_session_id(update)` from `agent-framework-hosting-telegram`, which can choose the chat, user, thread, or
|
||||
other Telegram-native partitioning logic for that helper;
|
||||
- `activity_session_id(activity)`, `discord_session_id(interaction_or_message)`, or
|
||||
`a2a_session_id(request_context)` from their respective protocol packages;
|
||||
- `foundry_user_isolation_key()` or `foundry_chat_isolation_key()` from `agent-framework-foundry-hosting`.
|
||||
|
||||
Keep these helpers outside `responses_to_run(...)`, `telegram_to_run(...)`, and other run-input parsers. That makes the
|
||||
trust boundary visible: using a request-derived key is a different decision than using a platform-provided isolation key.
|
||||
|
||||
The application builder is also responsible for deciding whether the hosting environment is **persistent** (for example,
|
||||
a long-running container or web app) or **transient** (for example, Azure Functions, Foundry Hosted Agents, or any
|
||||
environment where process memory is not a reliable continuity boundary). That decision controls which state mechanisms are
|
||||
safe to use:
|
||||
|
||||
- persistent single-process apps may use in-memory state for local development or simple deployments, while still needing
|
||||
durable state for multi-replica continuity;
|
||||
- transient apps must not rely on in-memory `SessionStore` state between calls and need a durable session store or a
|
||||
service-owned continuation id;
|
||||
- workflow hosts must choose an explicit `CheckpointStorage` and, when they need per-session resume, a durable
|
||||
`session_id -> checkpoint_id` cursor because in-process workflow state and in-memory checkpoint cursors do not survive
|
||||
transient execution.
|
||||
|
||||
A `SessionStore` stores `session_id -> AgentSession`, but it does not create sessions. `AgentState` resolves the agent
|
||||
target and creates the session on first use:
|
||||
|
||||
For agent targets:
|
||||
|
||||
```python
|
||||
session = await state.get_or_create_session(session_id)
|
||||
target = await state.get_target()
|
||||
result = await target.run(messages, session=session, options=options)
|
||||
```
|
||||
|
||||
If the protocol mints a new continuation id as part of the response being created (for example, OpenAI Responses
|
||||
`resp_*` ids), store the **post-run** session explicitly under that new id:
|
||||
|
||||
```python
|
||||
session = await state.get_or_create_session(previous_response_id)
|
||||
target = await state.get_target()
|
||||
result = await target.run(messages, session=session, options=options)
|
||||
await state.set_session(response_id, session)
|
||||
```
|
||||
|
||||
`agent.run(...)` may update the session object (for example, with service continuation state), so the explicit store call
|
||||
belongs after the run, not before it.
|
||||
|
||||
The session id is a partition key, not proof of identity. App or platform code must authenticate and authorize any
|
||||
externally supplied key before using it.
|
||||
|
||||
### Workflow checkpoints
|
||||
|
||||
Workflow checkpointing is execution state, not protocol state. `WorkflowState` pairs a workflow target with checkpoint
|
||||
state, but it should not wrap or replace the existing `CheckpointStorage` abstraction. Apps should pass the actual
|
||||
`CheckpointStorage` they want the workflow to use. If an app needs per-session resume, it can keep a small cursor from
|
||||
authorized `session_id` to `checkpoint_id` (or an equivalent store-specific resume token).
|
||||
|
||||
Workflow runs do not currently emit a checkpoint id on `WorkflowRunResult` or normal workflow events by default. The
|
||||
runner receives checkpoint ids internally from `CheckpointStorage.save(...)`. App/state code that owns the storage can
|
||||
observe the latest id by querying the storage after a run, for example
|
||||
`await storage.get_latest(workflow_name=target.name)`.
|
||||
|
||||
For workflow targets, app code adapts the protocol helper output into the workflow's expected input and invokes the
|
||||
workflow through the state object's target:
|
||||
|
||||
```python
|
||||
# session_id must already be authenticated and authorized for this caller
|
||||
target = await state.get_target()
|
||||
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
|
||||
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
|
||||
if latest is not None:
|
||||
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
|
||||
```
|
||||
|
||||
If a route wants to resume from a prior checkpoint, it explicitly chooses the checkpoint and passes it to
|
||||
`workflow.run(...)`:
|
||||
|
||||
```python
|
||||
# session_id must already be authenticated and authorized for this caller
|
||||
target = await state.get_target()
|
||||
checkpoint_id = await checkpoint_cursor_store.get(session_id)
|
||||
if checkpoint_id is None:
|
||||
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
|
||||
else:
|
||||
result = await target.run(checkpoint_id=checkpoint_id, checkpoint_storage=checkpoint_storage)
|
||||
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
|
||||
if latest is not None:
|
||||
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
|
||||
```
|
||||
|
||||
`workflow.run(...)` writes checkpoints to the provided storage, so storage selection must be explicit at the route layer.
|
||||
Protocol helper packages should not own checkpoint layout, route lifecycle, or durable execution.
|
||||
`state_dir` is limited to host-owned local files for reset-session aliases and workflow checkpoint path derivation. It does not store linked identities, active-channel state, response-routing state, continuation records, durable runner queues, or delivery attempts. Those storage concerns belong to ADR-0028.
|
||||
|
||||
## Non-goals for v1
|
||||
|
||||
The following remain outside the v1 protocol-helper contract. Some are deliberately app-owned in v1; others are possible
|
||||
future framework work only after a separate design.
|
||||
The following are deliberately **not** part of the v1 contract:
|
||||
|
||||
### App-owned in v1
|
||||
- cross-channel identity linking (`IdentityLinker`, `local_identity_link`, or `agent-framework-hosting-entra`),
|
||||
- identity allowlists or authorization policy (`IdentityAllowlist`, `AuthPolicy`),
|
||||
- response routing beyond the originating channel (`ResponseTarget`, active channel, specific linked channel, `all_linked`),
|
||||
- push or payload codecs (`ChannelPush`, `ChannelPushCodec`),
|
||||
- background/continuation delivery,
|
||||
- durable task runners (`DurableTaskRunner`, `InProcessTaskRunner`),
|
||||
- retry/replay policy (`RetryPolicy`),
|
||||
- fan-out, multicast, or all-linked delivery,
|
||||
- confidentiality tiers and `LinkPolicy`, and
|
||||
- a host-level multi-agent router.
|
||||
|
||||
The app builder owns these concerns with normal web-framework, SDK, platform, or application code:
|
||||
|
||||
- authentication, authorization policy, and allowlists;
|
||||
- deciding whether identities across protocols map to the same `session_id`;
|
||||
- non-originating sends using native SDK clients;
|
||||
- background work, durable execution, retry, and replay when app code owns the work;
|
||||
- routing between multiple agents.
|
||||
|
||||
This is easier in the protocol-helper model than it was in the host/channel model: app code already owns the native SDK
|
||||
clients, route handlers, authenticated caller context, session id selection, and outbound send calls. An app can link
|
||||
channels by choosing the same authorized `session_id` for multiple protocols, and can do non-originating delivery by
|
||||
calling the destination protocol's native client directly. That does not make a reusable framework feature safe by
|
||||
default; it just means the app-specific version no longer has to fight a host abstraction.
|
||||
|
||||
### Future framework work
|
||||
|
||||
The following require a reviewed identity, storage, delivery, replay, and observability model before becoming reusable
|
||||
framework features:
|
||||
|
||||
- reusable cross-channel identity linking;
|
||||
- framework-owned proactive or non-originating delivery;
|
||||
- fan-out, multicast, selected-channel, active-channel, or all-linked delivery;
|
||||
- framework-owned delivery observability, dead-letter handling, and replay semantics;
|
||||
- cross-channel confidentiality and link policy.
|
||||
|
||||
These possible framework enhancements are tracked by [ADR-0028](0028-hosting-linking-multicast-enhancements.md). They are
|
||||
not prerequisites for shipping or using the v1 protocol-helper surface. ADR-0028 was written against the earlier
|
||||
host/channel framing and must be revised to align with this protocol-helper and execution-state boundary before those
|
||||
enhancements are implemented.
|
||||
These areas are follow-up enhancements covered by [ADR-0028](0028-hosting-linking-multicast-enhancements.md). They are not prerequisites for shipping or using the v1 host.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- The released surface is smaller and easier to inspect: helpers plus state, not a channel framework.
|
||||
- Protocol helpers can be used from FastAPI, Starlette, Azure Functions, Django, CLI tools, tests, or native SDK webhook
|
||||
handlers.
|
||||
- App authors can use the authentication, dependency injection, lifecycle, and background-task tools they already know.
|
||||
- Session continuity stays explicit and debuggable.
|
||||
- Workflow checkpointing can still be centralized if needed without making protocol packages own routing.
|
||||
- The host/channel model can be implemented and tested without designing a security-sensitive identity graph.
|
||||
- Existing and new channel packages can share one Starlette app, middleware stack, lifecycle, and target invocation path.
|
||||
- Session continuity is explicit and debuggable: two channels share history only when they produce the same `isolation_key`.
|
||||
- Hook invocation is centralized in the host, so channels do not each invent the call convention.
|
||||
|
||||
Negative:
|
||||
|
||||
- Multi-protocol samples include explicit route/client code.
|
||||
- Apps that want a batteries-included ASGI app must write or depend on an app-specific wrapper.
|
||||
- Existing unreleased code and docs that mention channels, contribution, or hooks must be revised before release.
|
||||
- Apps that need OAuth linking, allowlists, proactive messages, or multicast must continue to implement those behaviors outside the v1 host.
|
||||
- Some richer cross-channel scenarios from the original design move to a separate decision and validation cycle.
|
||||
- The host must document `isolation_key` trust clearly because it now provides the shared session boundary.
|
||||
|
||||
## Validation Gates
|
||||
|
||||
Before this ADR is accepted:
|
||||
|
||||
- A sample can expose one target on multiple channels with one `AgentFrameworkHost` and no handwritten Starlette route composition.
|
||||
- Built-in channel tests prove that routes, commands, startup, and shutdown callbacks are contributed by channels and aggregated by the host.
|
||||
- Session tests prove that identical `ChannelSession.isolation_key` values resolve to the same cached `AgentSession`, and `reset_session` rotates that mapping.
|
||||
- Channel tests prove that each channel renders only its own originating response; there is no host-level push, multicast, or active-channel delivery path.
|
||||
- Workflow tests or samples use an explicit `checkpoint_location`.
|
||||
- Foundry isolation middleware is documented and covered by integration or contract tests, including the non-Foundry case where raw isolation headers are ignored.
|
||||
- The v1 API and packages do not expose the removed symbols or packages listed in [Non-goals for v1](#non-goals-for-v1).
|
||||
- The Python spec is updated to match this simplified contract and uses "public", "stable", or "released" terminology for Agent Framework APIs.
|
||||
|
||||
## More Information
|
||||
|
||||
- Follow-up linking and multicast ADR: [ADR-0028](0028-hosting-linking-multicast-enhancements.md). That ADR still uses
|
||||
some earlier host/channel terminology and must be aligned before implementation work starts.
|
||||
|
||||
## Appendix: Developer experience sketch
|
||||
|
||||
The examples below are sketches, not runtime-ready sample code. They show the minimum shape a developer would need to
|
||||
build: where protocol helpers are called, where app-owned auth/authorization belongs, where state is loaded/stored, and
|
||||
where native framework code remains in charge.
|
||||
|
||||
### Optional execution state
|
||||
|
||||
`AgentState` and `WorkflowState` stay small: they are target-specific state holders, not app hosts.
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
from agent_framework import AgentSession, SupportsAgentRun, Workflow
|
||||
|
||||
|
||||
class SupportsBuild(Protocol):
|
||||
def build(self) -> Workflow: ...
|
||||
|
||||
|
||||
class SessionStore:
|
||||
async def get(self, session_id: str) -> AgentSession | None: ...
|
||||
async def set(self, session_id: str, session: AgentSession) -> None: ...
|
||||
async def delete(self, session_id: str) -> None: ...
|
||||
|
||||
|
||||
class CheckpointCursorStore:
|
||||
async def get(self, session_id: str) -> str | None: ...
|
||||
async def set(self, session_id: str, checkpoint_id: str) -> None: ...
|
||||
async def delete(self, session_id: str) -> None: ...
|
||||
|
||||
|
||||
class AgentState:
|
||||
def __init__(self, target: SupportsAgentRun, *, session_store: SessionStore | None = None) -> None: ...
|
||||
async def get_target(self) -> SupportsAgentRun: ...
|
||||
async def get_or_create_session(self, session_id: str) -> AgentSession: ...
|
||||
async def set_session(self, session_id: str, session: AgentSession) -> None: ...
|
||||
|
||||
|
||||
class WorkflowState:
|
||||
def __init__(self, target: Workflow | SupportsBuild) -> None: ...
|
||||
async def get_target(self) -> Workflow: ...
|
||||
```
|
||||
|
||||
`WorkflowState` accepts direct `Workflow` instances, workflow factories, and builder-shaped objects with
|
||||
`build() -> Workflow`. That structurally covers `WorkflowBuilder` and the builders in `agent_framework_orchestrations`
|
||||
without making `agent-framework-hosting` depend on the orchestration package.
|
||||
|
||||
### Responses-only route
|
||||
|
||||
This sketch shows the intended Responses-only shape. The protocol package owns the Agent Framework run conversion helpers and
|
||||
response-id minting details; the application owns FastAPI routing, auth, policy adjustment, and response construction.
|
||||
|
||||
```python
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from agent_framework import Agent, ResponseStream
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework_hosting import AgentState # pyright: ignore[reportAttributeAccessIssue]
|
||||
from agent_framework_hosting_responses import create_response_id, responses_from_run, responses_from_streaming_run, responses_session_id, responses_to_run # pyright: ignore[reportAttributeAccessIssue]
|
||||
from fastapi import Body, FastAPI, Header, HTTPException
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
app = FastAPI()
|
||||
agent = Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="Assistant",
|
||||
instructions="Be concise and helpful.",
|
||||
)
|
||||
state = AgentState(agent)
|
||||
|
||||
|
||||
@app.post("/responses")
|
||||
async def responses(body: dict = Body(...), x_api_key: str | None = Header(default=None)) -> JSONResponse | StreamingResponse:
|
||||
if x_api_key != os.environ["RESPONSES_API_KEY"]:
|
||||
raise HTTPException(status_code=401, detail="bad api key")
|
||||
|
||||
# parse the request body into a set of AF objects
|
||||
run = responses_to_run(body)
|
||||
# get the candidate session id from the body
|
||||
# can be a resp_* for previous_response_id or a conv_* for a conversation
|
||||
candidate_session_id = responses_session_id(body)
|
||||
# create a new response_id for this run
|
||||
response_id = create_response_id()
|
||||
|
||||
# the developer can make any adjustments to the request, i.e.:
|
||||
run["options"]["store"] = False
|
||||
run["options"].pop("model", None)
|
||||
# the options here are of the shape defined by the ChatClient/Agent
|
||||
|
||||
# load the session (or create a new one) - this is optional
|
||||
# verify this caller owns candidate_session_id before loading it; API-key auth
|
||||
# alone does not prove ownership of a caller-supplied resp_* or conv_* id
|
||||
session_id = candidate_session_id or response_id
|
||||
session = await state.get_or_create_session(session_id)
|
||||
target = await state.get_target()
|
||||
|
||||
if run["stream"]:
|
||||
stream = target.run(
|
||||
run["messages"],
|
||||
stream=True,
|
||||
session=session,
|
||||
options=run["options"],
|
||||
)
|
||||
async def stream_events() -> AsyncIterator[str]:
|
||||
async for event in responses_from_streaming_run(
|
||||
stream,
|
||||
response_id=response_id,
|
||||
session_id=candidate_session_id,
|
||||
):
|
||||
yield event
|
||||
# agent.run may update the session during stream finalization, so store the post-run session explicitly
|
||||
await state.set_session(response_id, session)
|
||||
|
||||
return StreamingResponse(stream_events(), media_type="text/event-stream")
|
||||
|
||||
result = await target.run(
|
||||
run["messages"],
|
||||
session=session,
|
||||
options=run["options"],
|
||||
)
|
||||
# agent.run may update the session, so store the post-run session explicitly under the response id
|
||||
# this might also be skipped, if the app chooses to respect `store=False` policy
|
||||
await state.set_session(response_id, session)
|
||||
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
|
||||
|
||||
```
|
||||
|
||||
### Responses-only Django class-based view
|
||||
|
||||
The same helper surface can be used without FastAPI. A Django app owns URL routing, CSRF/auth policy, request parsing,
|
||||
and `JsonResponse` construction. In a real Django project this would live in the app's normal view module (for example
|
||||
`assistant/views.py`) and be routed from that app's `urls.py`; Django discovers it through its standard project/app
|
||||
layout, not through Agent Framework. This sketch shows the non-streaming path only; the streaming branch is the same
|
||||
state/finalization pattern shown in the FastAPI sketch and is omitted here to avoid duplicating it.
|
||||
|
||||
```python
|
||||
import json
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework_hosting import AgentState # pyright: ignore[reportAttributeAccessIssue]
|
||||
from agent_framework_hosting_responses import create_response_id, responses_from_run, responses_session_id, responses_to_run # pyright: ignore[reportAttributeAccessIssue]
|
||||
from django.http import HttpRequest, HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
|
||||
from django.views import View
|
||||
|
||||
|
||||
agent = Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="Assistant",
|
||||
instructions="Be concise and helpful.",
|
||||
)
|
||||
state = AgentState(agent)
|
||||
|
||||
|
||||
class ResponsesView(View):
|
||||
async def post(self, request: HttpRequest) -> JsonResponse:
|
||||
if request.headers.get("x-api-key") != os.environ["RESPONSES_API_KEY"]:
|
||||
return HttpResponseForbidden("bad api key")
|
||||
|
||||
try:
|
||||
body = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("invalid json")
|
||||
|
||||
run = responses_to_run(body)
|
||||
candidate_session_id = responses_session_id(body)
|
||||
response_id = create_response_id()
|
||||
options = run["options"]
|
||||
# verify this caller owns candidate_session_id before loading it; API-key auth
|
||||
# alone does not prove ownership of a caller-supplied resp_* or conv_* id
|
||||
session_id = candidate_session_id or response_id
|
||||
session = await state.get_or_create_session(session_id)
|
||||
target = await state.get_target()
|
||||
result = await target.run(
|
||||
run["messages"],
|
||||
session=session,
|
||||
options=options,
|
||||
)
|
||||
await state.set_session(response_id, session)
|
||||
return JsonResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
|
||||
```
|
||||
- Follow-up linking and multicast ADR: [ADR-0028](0028-hosting-linking-multicast-enhancements.md)
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-06-19
|
||||
deciders: eavanvalkenburg, moonbox3, TaoChenOSU, chetantoshnival
|
||||
consulted: westey-m
|
||||
informed:
|
||||
---
|
||||
|
||||
# Python identity lifetimes for sessions, tasks, and continuation
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Python `AgentSession` currently carries a local `session_id`, an optional opaque service continuation
|
||||
`service_session_id`, and provider state. `service_session_id` is any service-owned value that lets that service continue
|
||||
a conversation, session, or thread; chat clients happen to map it through the abstract `conversation_id` ChatOption, but
|
||||
other agent types can use it differently. It is not a generic correlation field, and generic correlation should not
|
||||
require parsing or understanding that opaque service-owned value.
|
||||
|
||||
The related issues mix values with different lifetimes:
|
||||
|
||||
- **Session / conversation identity**: values that group a multi-turn interaction. Examples: A2A `context_id`, OpenAI
|
||||
Responses `conversation` (`conv_*`) or response-chain continuation (`previous_response_id`).
|
||||
- **Task identity**: values that identify a protocol task and may affect future protocol calls. Example: A2A `task_id`.
|
||||
- **Message / response identity**: values that identify an output message or response. Examples: A2A `message_id` /
|
||||
`artifact_id`, OpenAI Responses response id (`resp_*`).
|
||||
- **Continuation token**: a framework resume payload for in-progress work. It may contain the same underlying value as a
|
||||
protocol id, such as A2A `task_id`, but it only exists when there is an unfinished operation to resume.
|
||||
|
||||
These values should not automatically live in the same object just because they all help "continue" something. A value
|
||||
belongs in `AgentSession` only when it is needed to continue future calls across turns. A value that identifies one
|
||||
result belongs on the response or message. A value that resumes in-progress work belongs in a `ContinuationToken`.
|
||||
|
||||
An `AgentSession` created for one agent is not expected to be guaranteed to work against another agent. When a session is
|
||||
used with an incompatible agent, protocol, or service, the framework should still help users understand what is wrong as
|
||||
early as possible, preferably before calling out to the remote service.
|
||||
|
||||
For #4673, native conversation identity propagation should be based on `AgentSession` where the value is durable session
|
||||
state. For #4893, A2A `context_id` and `task_id` need a coherent Agent Framework mapping.
|
||||
|
||||
AG-UI is out of scope for the decision. Its `thread_id` already maps to `AgentSession.session_id` in the normal wrapper
|
||||
path, and `run_id` is wrapper-owned event correlation. If AG-UI run correlation needs framework telemetry integration
|
||||
later, that should be handled as a run-context/telemetry design, not as session identity.
|
||||
|
||||
### Concrete gap example
|
||||
|
||||
At the protocol level, the durable continuation payload shapes are different:
|
||||
|
||||
```json
|
||||
// A2A: future calls may need multiple durable protocol fields
|
||||
{
|
||||
"context_id": "ctx_123",
|
||||
"task_id": "task_789",
|
||||
"task_state": "input_required"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// OpenAI Responses: future calls usually need one continuation value
|
||||
{
|
||||
"previous_response_id": "resp_abc123"
|
||||
}
|
||||
```
|
||||
|
||||
The gap is that A2A continuation state is multi-field while OpenAI continuation is
|
||||
typically single-field.
|
||||
|
||||
## Current implementation notes
|
||||
|
||||
- A2A currently has `A2AAgentSession`, but `A2AAgent.create_session(...)` does not automatically return it.
|
||||
- A2A currently mirrors `context_id` into `service_session_id`; that is current behavior, not necessarily the target
|
||||
abstraction.
|
||||
- A2A `task_id` is not just cosmetic correlation. It is used for `task_id` when a task is `INPUT_REQUIRED`, for
|
||||
`reference_task_ids` when refining a previous task, and inside `A2AContinuationToken` for in-progress tasks.
|
||||
- `RawAgent._prepare_run_context(...)` currently forwards `active_session.service_session_id` as chat `conversation_id`,
|
||||
so any non-string or formatted value affects existing chat-client paths.
|
||||
- `OpenAIChatClient` maps chat options `conversation_id` to the Responses API as `previous_response_id` for `resp_*`,
|
||||
`conversation` for `conv_*`, and defaults unrecognized strings to `previous_response_id`. When `store` is not `False`,
|
||||
it returns `response.conversation.id` when available, otherwise `response.id`, as the next service continuation value.
|
||||
- For Responses API, the response id (`resp_*`) is also the response/message identity surfaced as
|
||||
`ChatResponse.response_id`; when used for continuation on the next request, it becomes the `previous_response_id`
|
||||
value.
|
||||
- Python A2A has not been released as stable yet, so its session factory or session shape can still be adjusted before
|
||||
release.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Preserve `AgentSession.session_id` as the local/client conversation identity.
|
||||
- Preserve `AgentSession.service_session_id` as an opaque service-owned continuation handle.
|
||||
- Keep `AgentSession` for durable state needed across turns, not per-run bookkeeping.
|
||||
- Store values needed by future calls in durable session state; keep values that only resume in-progress work in
|
||||
`ContinuationToken`.
|
||||
- Fix the current confusion where session, task, response, and continuation values can be treated as interchangeable
|
||||
because they all participate in "continuing" something.
|
||||
- Make the implementation following this ADR preserve the lifetime split clearly: future-call state, in-progress resume
|
||||
tokens, response/message ids, and protocol event correlation must not be silently mixed.
|
||||
- Expose durable continuation state in a typed way when future calls depend on it.
|
||||
- Let telemetry correlate runs without parsing opaque service continuation handles.
|
||||
- Reuse existing run/context surfaces before introducing a new identity abstraction.
|
||||
- Keep MCP and other remote tool boundaries safe: framework identity must not be forwarded to remote tools unless an
|
||||
existing explicit opt-in mechanism says so.
|
||||
- Keep existing `AgentSession.to_dict()` / `from_dict()` migration and compatibility straightforward.
|
||||
- Stay close to .NET where there is already behavior to match, especially A2A's `ContextId`, `TaskId`, and `TaskState`.
|
||||
- Detect incompatible session identity shapes as early as practical, preferably before a remote service call.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not design a provider-agnostic conversation creation API here. That is tracked separately in #6622.
|
||||
- Do not make `service_session_id` a generic telemetry or run-correlation field.
|
||||
- Do not introduce a new identity object if existing run/context objects can carry the selected per-run correlation value.
|
||||
- Do not make a session from one agent guaranteed to work against another agent.
|
||||
- Do not optimize the public `agent.run(...)` API for protocol-wrapper internals.
|
||||
|
||||
## Remaining question: durable shape for additional continuation state
|
||||
|
||||
- Option A: Use protocol-specific `AgentSession` subclasses.
|
||||
- Option B: Extend `service_session_id` with richer service-owned values.
|
||||
- Option C: Add a dedicated dict for additional session details.
|
||||
- Option D: Store additional durable state inside `AgentSession.state`.
|
||||
|
||||
### Option A: Use protocol-specific `AgentSession` subclasses
|
||||
|
||||
Each protocol or agent type that needs additional durable state keeps a specialized `AgentSession` subclass. For A2A,
|
||||
that means keeping `A2AAgentSession` for A2A-specific durable state and changing `A2AAgent.create_session(...)` to return
|
||||
that type.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
# First call returns a task that future A2A messages may need to reference.
|
||||
session = await a2a_agent.create_session()
|
||||
|
||||
response = await a2a_agent.run(
|
||||
message,
|
||||
session=session,
|
||||
)
|
||||
|
||||
# A2AAgent updates durable A2A protocol state from the returned task/status payload.
|
||||
# The user does not set these manually.
|
||||
assert isinstance(session, A2AAgentSession)
|
||||
assert session.task_id is not None
|
||||
assert session.task_state is not None
|
||||
|
||||
# Later call reuses the durable A2A session state. A2AAgent decides whether to send task_id
|
||||
# for INPUT_REQUIRED or reference_task_ids for task refinement.
|
||||
next_response = await a2a_agent.run(
|
||||
next_message,
|
||||
session=session,
|
||||
)
|
||||
```
|
||||
|
||||
- Good, because protocol-specific state stays in a protocol-specific type.
|
||||
- Good, because it aligns with .NET A2A's `A2AAgentSession` shape.
|
||||
- Good, because Python A2A can still make this pre-release session factory adjustment.
|
||||
- Good, because `task_state` does not get promoted to a base `AgentSession` concept.
|
||||
- Bad, because generic consumers cannot read protocol-specific state without knowing about the subclass or a helper API.
|
||||
- Bad, because it depends on each subclass consistently setting shared session fields such as `service_session_id` where
|
||||
those are part of the shared abstraction.
|
||||
|
||||
### Option B: Extend `service_session_id` with richer service-owned values
|
||||
|
||||
Keep the common `service_session_id` case as a plain string. When an agent/service needs more than one service-owned
|
||||
continuation value, allow `service_session_id` to be a typed structured value, such as a `TypedDict`. The main session ID
|
||||
used for `gen_ai.conversation.id` should still be extracted by the owning agent, not inferred by generic telemetry code.
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
simple_session = AgentSession(
|
||||
service_session_id="resp_123",
|
||||
)
|
||||
|
||||
structured_session = AgentSession(
|
||||
service_session_id=A2AServiceSessionId(
|
||||
context_id="ctx_123",
|
||||
task_id="task_789",
|
||||
task_state=TaskState.TASK_STATE_WORKING,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
- Good, because the common case remains a plain string and stays simple.
|
||||
- Good, because richer service-owned continuation state stays under the existing continuation property.
|
||||
- Good, because a structured value can make framework-side validation possible before a value is sent back to a service.
|
||||
- Good, because A2A can keep `context_id`, `task_id`, and `task_state` together as the service/protocol-owned continuation
|
||||
value without adding A2A fields to base `AgentSession`.
|
||||
- Neutral, because telemetry needs an agent-owned extractor to pick the `gen_ai.conversation.id` value from either a
|
||||
string or structured `service_session_id`.
|
||||
- Neutral, because Python A2A would need a pre-release adjustment to stop relying on `A2AAgentSession` for these fields.
|
||||
- Bad, because changing the `service_session_id` type is a compatibility risk for users, providers, serialization, and
|
||||
tests.
|
||||
- Bad, because every path that sends `service_session_id` back to a service must consistently extract/adapt the
|
||||
service-owned continuation component.
|
||||
|
||||
### Option C: Add a dedicated dict for additional session details
|
||||
|
||||
Keep `service_session_id` as the primary opaque service-owned continuation handle, and add a separate dictionary for
|
||||
additional durable protocol/service values that need to travel with the session.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
session = AgentSession(
|
||||
service_session_id="ctx_123",
|
||||
session_details={
|
||||
"task_id": "task_456",
|
||||
"task_state": TaskState.TASK_STATE_WORKING,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
- Good, because the main service continuation handle stays a plain `service_session_id` string.
|
||||
- Good, because extra state has an explicit home and does not overload `service_session_id`.
|
||||
- Good, because generic consumers can look in one documented place for additional session-scoped values.
|
||||
- Neutral, because helper APIs can hide the raw dictionary access.
|
||||
- Bad, because this still introduces string-keyed state unless the dict values are wrapped by typed helpers.
|
||||
- Bad, because it adds another public session field that needs serialization, naming, and compatibility rules.
|
||||
- Bad, because generic consumers still need to understand the shape or use helpers for the selected agent/session type.
|
||||
|
||||
### Option D: Store additional durable state inside `AgentSession.state`
|
||||
|
||||
Keep base `AgentSession` unchanged and store additional durable continuation/protocol state under namespaced keys in
|
||||
`session.state`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
session = AgentSession(session_id="ctx_123")
|
||||
|
||||
session.state["a2a"] = {
|
||||
"task_id": "task_456",
|
||||
"task_state": TaskState.TASK_STATE_WORKING,
|
||||
}
|
||||
```
|
||||
|
||||
- Good, because it avoids new public fields and avoids a subclass requirement.
|
||||
- Good, because `AgentSession.state` already exists for provider/session state.
|
||||
- Neutral, because helper APIs can hide the raw dictionary access.
|
||||
- Bad, because stringly typed state is easier to corrupt and harder to validate.
|
||||
- Bad, because generic consumers need helper APIs anyway; directly reading nested dictionaries is not a good abstraction.
|
||||
- Bad, because users may accidentally overwrite or persist invalid protocol state.
|
||||
|
||||
## Decision
|
||||
|
||||
Chosen decision criteria for the future: **split identity by lifecycle**.
|
||||
|
||||
When a protocol emits an id/token, place it by answering "what lifecycle does this value serve?":
|
||||
|
||||
- **Future-call continuation state** -> durable session state. Examples: A2A `context_id` + `task_id` + `task_state`;
|
||||
OpenAI Responses `previous_response_id`/`conversation`.
|
||||
- **Single-result identity** -> response/message object only. Examples: OpenAI `resp_*`, A2A `message_id`,
|
||||
A2A `artifact_id`.
|
||||
- **Resume unfinished work** -> `ContinuationToken` only. Example: a token carrying in-progress task resume data.
|
||||
- **Run-start-only request fields** -> run method arguments/options, not durable session state. Example: A2A
|
||||
`reference_task_ids` for a specific follow-up/refinement request.
|
||||
- **Per-run correlation/telemetry** -> protocol wrapper or run context, not `AgentSession`. Example: wrapper-managed
|
||||
`run_id` used only for tracing/events.
|
||||
|
||||
Durable-state option decision: **Option B: Extend `service_session_id` with richer service-owned values**.
|
||||
This does **not** add a new top-level identity abstraction; it keeps continuation identity under
|
||||
`service_session_id` and keeps run correlation in existing run/telemetry context.
|
||||
The immediate implementation gap is mainly in A2A mapping clarity, but the lifecycle split applies
|
||||
consistently across providers.
|
||||
|
||||
To support telemetry, `BaseAgent` should expose a method that accepts an `AgentSession | None` and returns the value to
|
||||
use for `gen_ai.conversation.id`. The default implementation should return `session.service_session_id` when it is a
|
||||
string. Agents that use a structured `service_session_id`, such as `A2AAgent`, should override that method and return the
|
||||
appropriate primary session/context value.
|
||||
|
||||
## Appendix: A2A `task_id` and `reference_task_ids` implementation check
|
||||
|
||||
The A2A protocol distinguishes a message's `task_id` from `reference_task_ids`:
|
||||
|
||||
- `task_id` associates the message with a specific task.
|
||||
- `reference_task_ids` provides additional task context, for example when a new task refines or follows up on the result
|
||||
of a previous task.
|
||||
|
||||
The protocol does not appear to prescribe that `task_id` and `reference_task_ids` are mutually exclusive. If both are
|
||||
present, the natural reading is that the message is associated with one task while also referencing other tasks for
|
||||
context. The serving agent decides how to interpret that context.
|
||||
|
||||
The Python implementation should check and likely adjust the current behavior:
|
||||
|
||||
- `task_id` should be updated by the current run when the remote A2A service returns a task/status payload.
|
||||
- `task_id` should remain durable A2A session state when needed for future calls, for example when a task is
|
||||
`INPUT_REQUIRED`.
|
||||
- `reference_task_ids` should be a run parameter / caller intent for the current request, not implicit durable session
|
||||
continuation state.
|
||||
- A follow-up/refinement request should pass explicit `reference_task_ids` when it wants to reference previous tasks.
|
||||
- If both session `task_id` and run `reference_task_ids` are present, the wrapper should preserve the protocol
|
||||
distinction rather than treating one as a replacement for the other.
|
||||
- If no `reference_task_ids` are supplied, the wrapper should not automatically infer them from the last session task
|
||||
unless we deliberately keep that convenience for compatibility.
|
||||
|
||||
## Appendix: implementation notes for Option B
|
||||
|
||||
The exact names are implementation details, but the shape should be:
|
||||
|
||||
```python
|
||||
class A2AServiceSessionId(TypedDict):
|
||||
context_id: str
|
||||
task_id: str | None
|
||||
task_state: TaskState | None
|
||||
|
||||
|
||||
class AgentSession:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
session_id: str | None = None,
|
||||
service_session_id: str | ServiceSessionId | None = None,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
def _get_otel_conversation_id(self, session: AgentSession | None) -> str | None:
|
||||
service_session_id = session.service_session_id if session else None
|
||||
return service_session_id if isinstance(service_session_id, str) else None
|
||||
|
||||
|
||||
class A2AAgent(BaseAgent):
|
||||
def _get_otel_conversation_id(self, session: AgentSession | None) -> str | None:
|
||||
service_session_id = session.service_session_id if session else None
|
||||
if isinstance(service_session_id, Mapping):
|
||||
return service_session_id.get("context_id")
|
||||
return service_session_id if isinstance(service_session_id, str) else None
|
||||
|
||||
|
||||
class AgentTelemetryLayer:
|
||||
def _trace_agent_invocation(...):
|
||||
attributes = _get_span_attributes(
|
||||
...,
|
||||
thread_id=self._get_otel_conversation_id(session),
|
||||
...,
|
||||
)
|
||||
```
|
||||
|
||||
This keeps the OpenTelemetry extraction decision with the agent that owns the service continuation shape. Generic OTel
|
||||
code should not parse structured `service_session_id` values directly.
|
||||
|
||||
`AgentSession` must also be updated so `service_session_id` can store either the current string value or a structured
|
||||
service-owned value. Serialization must preserve both shapes, and existing serialized sessions with string
|
||||
`service_session_id` must continue to round-trip unchanged.
|
||||
|
||||
## More Information
|
||||
|
||||
Related work and issues:
|
||||
|
||||
- #4673: native conversation ID propagation.
|
||||
- #4893: align A2A protocol concepts with Agent Framework session/continuation concepts.
|
||||
- #2931: Foundry-specific conversation creation helper, split into a separate Python PR.
|
||||
- #6622: broader provider-agnostic conversation creation API discussion requiring .NET sync.
|
||||
- [ADR-0015](0015-agent-run-context.md): AgentRunContext for Agent Run.
|
||||
- [ADR-0018](0018-agentthread-serialization.md): AgentSession serialization.
|
||||
- [ADR-0026](0026-hosted-session-identity-context.md): hosted session identity context.
|
||||
@@ -1,84 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: rogerbarreto
|
||||
date: 2026-06-29
|
||||
deciders: rogerbarreto
|
||||
consulted: []
|
||||
informed: []
|
||||
---
|
||||
|
||||
# Hosted platform context (user id + call id) for Foundry Hosting on AgentServer 2.0
|
||||
|
||||
Supersedes [ADR-0026](0026-hosted-session-identity-context.md).
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
[ADR-0026](0026-hosted-session-identity-context.md) sourced the hosted-agent end-user identity from `ResponseContext.Isolation` (an `IsolationContext` typed `UserIsolationKey` / `ChatIsolationKey`), injected by the platform as the `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers.
|
||||
|
||||
`Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) removes that surface. `ResponseContext.Isolation` is gone; the platform now exposes `ResponseContext.PlatformContext` (a `PlatformContext` typed `UserIdKey` and `CallId`), populated from the `x-agent-user-id` and `x-agent-foundry-call-id` headers. The chat isolation key no longer exists, and a new per-request **call id** is introduced that first-party Foundry services (the toolbox proxy in particular) require on outbound calls to resolve the server-side-stored caller context. The hosting layer in `Microsoft.Agents.AI.Foundry.Hosting` had to migrate to this contract without changing the public shape that samples and providers depend on.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Track the breaking `Azure.AI.AgentServer.*` 2.0.0 surface (`PlatformContext` replacing `Isolation`) while keeping the same per-user partitioning guarantees from ADR-0026.
|
||||
- Keep the change **internal**: existing hosted samples and `AIContextProvider`s must not need code changes. `session.GetHostedContext().UserId`, `HostedSessionIsolationKeyProvider`, and `AddFoundryResponses` stay source-compatible.
|
||||
- Forward the new per-request call id verbatim on outbound calls to Foundry first-party services so per-user toolbox OAuth consent and other server-side caller-context lookups keep working.
|
||||
- Remain resilient on protocol `1.0.0`: when only the legacy headers are present, `UserIdKey` still resolves and `CallId` is simply absent.
|
||||
- Preserve the strict-resume tamper defense from ADR-0026 with identity now reduced to user only.
|
||||
|
||||
## Considered Options
|
||||
|
||||
For the identity source:
|
||||
|
||||
1. **Map `ResponseContext.PlatformContext.UserIdKey`** into the existing `HostedSessionContext` (user only), keeping ADR-0026's storage shape and read accessor.
|
||||
2. Keep a `ChatId` slot on `HostedSessionContext` for backward source-compatibility, populated from `CallId` or left null.
|
||||
|
||||
For the call id propagation:
|
||||
|
||||
A. **A request-scoped ambient (`HostedCallContext`, an `AsyncLocal<string?>`)** set by the handler and re-applied before each egress point, read by the outbound delegating handler.
|
||||
B. Thread the call id through every method signature down to the toolbox bearer handler.
|
||||
|
||||
For session keying (previously implied by the conversation/chat pairing):
|
||||
|
||||
I. **`HostedConversationKey`** resolving a stable partition from `conversation_id ?? partition(previous_response_id) ?? partition(responseId)`.
|
||||
II. Continue keying on the container session id (`FOUNDRY_AGENT_SESSION_ID`).
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen: **Option 1** for identity, **Option A** for call id, **Option I** for session keying.
|
||||
|
||||
Rationale:
|
||||
|
||||
- **`ChatId` dropped (Option 2 rejected).** The platform no longer supplies a chat key; carrying a synthetic one would invent identity the trust boundary does not provide. `HostedSessionContext` becomes user-only (`HostedSessionContext(string userId)` / `UserId`), and the strict-resume check validates `UserId` alone. The corresponding `HostedFoundryMemoryProviderScopes` values `PerChat` and `PerUserAndChat` are removed; `PerUser` is retained.
|
||||
- **Ambient call id (Option B rejected).** Writing `HostedCallContext.CallId` inside the streaming `async IAsyncEnumerable` iterator is reverted across each `yield`, so a single up-front assignment is lost before the toolbox/MCP egress runs. The handler therefore captures `context.PlatformContext?.CallId` once and **re-applies it immediately before each egress point**; `FoundryToolboxBearerTokenHandler` forwards it as `x-agent-foundry-call-id`. The ambient is request-scoped and never leaks into the caller's execution context (guarded by a unit test).
|
||||
- **`HostedConversationKey` (Option II rejected).** One container serves many conversations for its lifetime, so the container session id cannot key per-conversation state. The partition key is derived from the conversation/`previous_response_id`/minted response id instead.
|
||||
|
||||
Implementation summary in `Microsoft.Agents.AI.Foundry.Hosting`:
|
||||
|
||||
| Type | Visibility | Change vs ADR-0026 |
|
||||
|---|---|---|
|
||||
| `HostedSessionContext` | public sealed | Now user-only (`UserId`); `ChatId` removed. |
|
||||
| `PlatformHostedSessionIsolationKeyProvider` | internal sealed | Maps `context.PlatformContext.UserIdKey` (was `context.Isolation.UserIsolationKey` / `ChatIsolationKey`). |
|
||||
| `HostedCallContext` | internal static | New. Request-scoped `AsyncLocal<string?>` holding the `x-agent-foundry-call-id` value. |
|
||||
| `HostedConversationKey` | internal | New. Resolves the per-conversation partition key. |
|
||||
| `FoundryToolboxBearerTokenHandler` | internal | Now also forwards `x-agent-foundry-call-id` outbound. |
|
||||
| `HostedFoundryMemoryProviderScopes` | public | `PerChat` / `PerUserAndChat` removed; `PerUser` kept. |
|
||||
|
||||
Package manifests bump the responses container protocol to `2.0.0` (invocations stays `1.0.0`).
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- Per-user memory partitioning and the strict-resume tamper defense from ADR-0026 are preserved with no public API churn for samples or providers.
|
||||
- Per-user toolbox OAuth consent and other server-side caller-context lookups keep working because the per-request call id is forwarded on egress.
|
||||
- Works unchanged on protocol `1.0.0` (no call id) and `2.0.0`.
|
||||
|
||||
Negative:
|
||||
|
||||
- `HostedSessionContext.ChatId` and the `PerChat` / `PerUserAndChat` memory scopes are removed; any out-of-tree consumer that referenced them must move to user-scoped partitioning.
|
||||
- The call id must be re-applied before every egress point because of the async-iterator `AsyncLocal` revert; a missed re-apply silently drops the header. This is covered by unit tests.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- HMAC tamper signatures over the persisted context remain unimplemented; equality comparison against `ResponseContext.PlatformContext` on every request is sufficient because the platform sets the header at the trust boundary.
|
||||
- The per-request `User` field on `CreateResponse` is still intentionally not consumed.
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
status: accepted
|
||||
contact: rogerbarreto
|
||||
date: 2026-06-30
|
||||
deciders: rogerbarreto
|
||||
consulted: []
|
||||
informed: []
|
||||
---
|
||||
|
||||
# Per-agent and per-user session-storage isolation for Foundry Hosting
|
||||
|
||||
Builds on [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
A Foundry hosted container can serve many end users (and, in .NET, many agents) over its lifetime. The
|
||||
`AgentSessionStore` persists each turn's `AgentSession` (which for a workflow agent carries the workflow
|
||||
checkpoint, and which also carries the tool-approval mapping via `ToolApprovalIdMap` in the session state
|
||||
bag). [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md) protected cross-user access only through
|
||||
the strict-resume identity check (a 403 when the persisted `HostedSessionContext.UserId` does not match the
|
||||
live request). The persisted artifacts themselves were keyed by `conversationId` (+ agent name), not
|
||||
physically partitioned per user, so a forged `conversation_id` would still resolve to another user's file
|
||||
path before the identity check rejected it.
|
||||
|
||||
The Python hosting package added physical per-user partitioning (`<root>/<user_id>/<context_id>`) plus a
|
||||
reject-style path-traversal guard. We want .NET to provide the same defense-in-depth, adapted to the .NET
|
||||
hosting model.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- Defense in depth: a forged/guessed id must not even resolve to another tenant's storage path, independent
|
||||
of the identity check.
|
||||
- Multi-agent hosting: a single .NET container hosts multiple agents resolved from keyed DI, so the layout
|
||||
must isolate per agent as well as per user (Python hosts a single agent and needs no agent layer).
|
||||
- Path-traversal safety (CWE-22) for the untrusted, platform-injected user id.
|
||||
- Back-compat for local development (no `x-agent-user-id` header) and for direct/non-hosted store use.
|
||||
- Keep the change contained and avoid the async-iterator `AsyncLocal` revert hazard from ADR-0030.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Path partition inside `FileSystemAgentSessionStore`**, threading the user id explicitly through the
|
||||
`AgentSessionStore` API, with self-describing prefixed segments.
|
||||
- A delegating store that prefixes the conversation id with the user id (the
|
||||
`IsolationKeyScopedAgentSessionStore` pattern from `Microsoft.Agents.AI.Hosting`). Rejected: still needs the
|
||||
user id on the read path and yields a flat key rather than nested per-tenant directories.
|
||||
- An `AsyncLocal<string?>` user-context set by the handler. Rejected: the session is saved in the handler's
|
||||
`finally` after the streaming `yield`s, where an `AsyncLocal` set up front is reverted (the same hazard
|
||||
that forced explicit call-id re-application in ADR-0030). Explicit threading is safer and clearer.
|
||||
- A separate per-user approval store (as in Python). Rejected as unnecessary: see below.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Path layout with self-describing, prefixed segments; user id threaded explicitly:
|
||||
|
||||
{root}/ a-{agentName} / u-{userId} / c-{contextId}.json
|
||||
|
||||
- `a-` (agent), `u-` (user), `c-` (context) are constant literals applied to the sanitized/validated value,
|
||||
so a collapsed layout is never ambiguous and a user id can never masquerade as an agent name.
|
||||
- `contextId` is `HostedConversationKey.Resolve` (conversation_id, else the partition of
|
||||
previous_response_id, else of the minted response id).
|
||||
- The agent and context layers are always present (Foundry always deploys a named agent). The only
|
||||
collapse is the `u-` layer: present when a user id is resolved (Foundry header, or local dev fallback),
|
||||
absent for raw local runs with no header (`{root}/a-{agent}/c-{conv}.json`). There is no user-only or
|
||||
no-agent layout.
|
||||
|
||||
Other elements:
|
||||
|
||||
- `string? userId` was added as a **required** parameter (no default) on `AgentSessionStore.GetSessionAsync` /
|
||||
`SaveSessionAsync` (a contained, breaking change to the experimental Foundry abstraction; both in-tree
|
||||
implementations and the two handler call sites were updated). It is required rather than optional so a
|
||||
caller can never silently persist a session unscoped; a genuine no-user caller (local without the header,
|
||||
or a non-hosted direct caller) passes `null` explicitly. `AgentFrameworkResponseHandler` resolves the user
|
||||
id before loading the session.
|
||||
- Path-traversal guard: the user id is rejected (not sanitized) when it is not a single safe path segment
|
||||
(path separators, NUL, drive letters, rooted paths, all-dot segments). After building the path, the
|
||||
fully-resolved path is asserted to remain under the storage root.
|
||||
- The strict-resume 403 identity check from ADR-0030 is **kept** as the second defense layer (it still
|
||||
catches a session that reaches the wrong partition, e.g. via a non-partitioning custom store or in-process
|
||||
tampering).
|
||||
- **No separate approval store.** The tool-approval mapping lives in `ToolApprovalIdMap` ->
|
||||
`AgentSessionStateBag`, which is serialized into the session checkpoint, so partitioning the session path
|
||||
isolates pending approvals per tenant automatically. (Python needs a separate per-user approval store only
|
||||
because it models approvals as a standalone store.)
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- Cross-tenant isolation is now defense-in-depth: physical per-agent/per-user partitioning plus the identity
|
||||
check. Approvals and workflow checkpoints inherit the partitioning because they ride in the session.
|
||||
- Self-describing prefixes make the on-disk layout auditable and collision-free across collapse cases.
|
||||
|
||||
Negative:
|
||||
|
||||
- Breaking change to the experimental Foundry `AgentSessionStore` API (added `userId`).
|
||||
- The on-disk layout and leaf filename change (`<conv>.json` -> `c-<conv>.json`), orphaning sessions written
|
||||
by the ADR-0030 release. Acceptable for an experimental package; a fresh session is created on next use.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Encryption at rest and quota enforcement remain platform concerns.
|
||||
- Non-Foundry hosting layers can adopt an equivalent scheme independently.
|
||||
|
||||
## Update (2026-07-01): local runs no longer fail closed; sample dev provider removed
|
||||
|
||||
Superseding the ADR-0026/0030 behavior where a `null` result from `HostedSessionIsolationKeyProvider`
|
||||
always became a 500, `AgentFrameworkResponseHandler` now branches on `FoundryEnvironment.IsHosted`:
|
||||
|
||||
- **Hosted** (`IsHosted == true`, production): a `null` identity is still a hard error (500). Isolation
|
||||
stays strict; the platform always injects `x-agent-user-id`.
|
||||
- **Not hosted** (local `docker run` / `dotnet run`): a `null` identity is tolerated. Per-user isolation
|
||||
is simply not triggered — the handler passes `userId == null` to the store (the documented "no user
|
||||
partition", `{root}/a-{agent}/c-{conv}.json`), stamps no `HostedSessionContext`, and runs no
|
||||
strict-resume check. Contributors can run a hosted image locally with zero extra setup.
|
||||
|
||||
Consequently the sample-side `DevTemporaryLocalUserIdProvider` and `AddDevTemporaryLocalContributorSetup`
|
||||
were removed. To simulate distinct users locally, send an `x-agent-user-id` request header; the default
|
||||
`PlatformHostedSessionIsolationKeyProvider` reads it via `ResponseContext.PlatformContext.UserIdKey`
|
||||
(the SDK's `PlatformContext.FromRequest` populates it from the header unconditionally, hosted or not).
|
||||
@@ -8,8 +8,6 @@
|
||||
- **Context Provider Pattern** - `SecureAgentConfig` extends `ContextProvider`, injecting tools, instructions, and middleware automatically
|
||||
- **Automatic Variable Hiding** - UNTRUSTED content is automatically hidden without requiring manual intervention
|
||||
- **Per-Item Embedded Labels** - Tools return `list[Content]` with `Content.from_text()` for proper label propagation
|
||||
- **SecureMCPToolProxy Auto-Labeling** - MCP tools are labeled automatically from MCP `ToolAnnotations` hints
|
||||
- **MCP `_meta.ifc` Support** - Per-result IFC labels from servers (for example GitHub MCP with `X-MCP-Features: ifc_labels`) are parsed and enforced
|
||||
- **SecureAgentConfig** - One-line secure agent configuration via `context_providers=[config]`
|
||||
- **Data Exfiltration Prevention** - `max_allowed_confidentiality` prevents sensitive data leakage
|
||||
- **Message-Level Label Tracking** (Phase 1) - Track labels on every message in the conversation
|
||||
@@ -25,7 +23,6 @@ The FIDES defense system consists of seven main components:
|
||||
5. **Security Tools** - Specialized tools for safe handling of untrusted content (`quarantined_llm`, `inspect_variable`)
|
||||
6. **SecureAgentConfig** - Context provider for easy secure agent configuration
|
||||
7. **Message-Level Label Tracking** - Track labels on every message in the conversation (Phase 1)
|
||||
8. **MCP Tool/Result Label Integration** - MCP hint-based tool labeling and `_meta.ifc` result label parsing
|
||||
|
||||
## Implementation Details
|
||||
|
||||
@@ -187,17 +184,6 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
### 7. MCP Labeling Pipeline (Hints + `_meta.ifc`)
|
||||
|
||||
FIDES now secures remote MCP integration end-to-end:
|
||||
|
||||
- **Tool labels from hints**: `apply_mcp_security_labels(...)` maps MCP hints (`readOnlyHint`, `openWorldHint`) to FIDES tool properties.
|
||||
- **Safe sink defaults**: tools not explicitly marked `readOnlyHint=True` are treated as potential sinks and receive `max_allowed_confidentiality=public`.
|
||||
- **Result labels from metadata**: MCP result `_meta` is propagated via `__mcp_result_meta__`; `_meta.ifc` is parsed into `security_label` per result item.
|
||||
- **`SecureMCPToolProxy` convenience**: wraps MCP tools/URLs and applies this labeling automatically on connect.
|
||||
|
||||
This behavior is used with the GitHub MCP server when `X-MCP-Features: ifc_labels` is passed, which causes the server to return IFC labels in `_meta` (for example `{"ifc": {"integrity": "untrusted", "confidentiality": "public"}}`).
|
||||
|
||||
## Security Properties
|
||||
|
||||
### Deterministic Defense
|
||||
|
||||
@@ -1,348 +1,320 @@
|
||||
---
|
||||
status: proposed
|
||||
contact: eavanvalkenburg
|
||||
date: 2026-07-08
|
||||
date: 2026-06-11
|
||||
deciders: eavanvalkenburg
|
||||
---
|
||||
|
||||
# Python protocol helpers and optional execution state
|
||||
# Python hosting core and pluggable channels
|
||||
|
||||
## Scope
|
||||
|
||||
This specification is the Python implementation plan for
|
||||
[ADR-0027](../decisions/0027-hosting-channels.md). It documents the helper-first v1 contract for Python hosting.
|
||||
This specification is the Python implementation plan for [ADR-0027](../decisions/0027-hosting-channels.md). It documents the simplified v1 host/channel contract only.
|
||||
|
||||
The v1 contract is:
|
||||
|
||||
- protocol packages expose helper functions that convert protocol-native input to Agent Framework run values;
|
||||
- protocol packages expose helper functions that convert Agent Framework run results or streams back to protocol-native
|
||||
payloads or operations;
|
||||
- application/framework code owns routes, native SDK clients, authentication, command policy, webhooks, response status
|
||||
codes, and outbound sends;
|
||||
- `agent-framework-hosting` provides small optional state holders for Agent Framework targets;
|
||||
- state helpers do not own web apps, route contribution, protocol dispatch, command projection, or native SDK calls.
|
||||
- `AgentFrameworkHost` owns one Starlette app, one hostable target, and one or more channels.
|
||||
- A hostable target is either a `SupportsAgentRun`-compatible agent or a `Workflow`.
|
||||
- Channels contribute routes, middleware, commands, and lifecycle callbacks.
|
||||
- Channels parse protocol-native input into `ChannelRequest`.
|
||||
- Channels render their own originating response.
|
||||
- Session continuity is explicit: a channel supplies `ChannelSession(isolation_key=...)`, and the host resolves/caches an `AgentSession` for that key.
|
||||
- The host invokes `ChannelRunHook` and `ChannelResponseHook`; channels provide hook configuration and protocol context.
|
||||
|
||||
The host does not link identities, route responses to other channels, run background continuations, or multicast in v1. Those enhancements are tracked in [ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md).
|
||||
|
||||
## Goals
|
||||
|
||||
- Let apps expose agents and workflows from FastAPI, Starlette, Django, Azure Functions, native SDK webhooks, CLIs, and
|
||||
tests without adopting a host/channel framework.
|
||||
- Keep protocol parsing and response formatting inside protocol packages.
|
||||
- Keep session continuity explicit and app-owned at the trust boundary.
|
||||
- Reuse Agent Framework primitives: `AgentSession`, `CheckpointStorage`, `Agent.run(...)`, `Workflow.run(...)`, and
|
||||
`ResponseStream`.
|
||||
- Preserve full-fidelity Agent Framework results until a protocol helper renders them.
|
||||
- Let an app expose one agent or workflow on multiple protocols without handwritten Starlette composition.
|
||||
- Keep protocol parsing and response formatting inside channel packages.
|
||||
- Provide one session-resolution path shared by all channels.
|
||||
- Keep the channel authoring surface small enough for new channels to implement.
|
||||
- Preserve full-fidelity agent and workflow results until a channel decides how to render them.
|
||||
|
||||
## Non-goals for v1
|
||||
|
||||
### App-owned in v1
|
||||
The following are removed from the v1 implementation pass:
|
||||
|
||||
The app builder owns these concerns with normal web-framework, SDK, platform, or application code:
|
||||
- `IdentityLinker`, `IdentityAllowlist`, `AuthPolicy`, and `LinkPolicy`
|
||||
- `ResponseTarget`, active-channel routing, `all_linked`, fan-out, and multicast
|
||||
- `ChannelPush` and `ChannelPushCodec`
|
||||
- `DurableTaskRunner`, `InProcessTaskRunner`, and `RetryPolicy`
|
||||
- continuation tokens and background delivery
|
||||
- confidentiality tiers
|
||||
- `agent-framework-hosting-entra`
|
||||
- `local_identity_link`
|
||||
|
||||
- authentication, authorization policy, and allowlists;
|
||||
- deciding whether identities across protocols map to the same `session_id`;
|
||||
- non-originating sends using native SDK clients;
|
||||
- background work, durable execution, retry, and replay when app code owns the work;
|
||||
- routing between multiple agents.
|
||||
|
||||
The helper-first model makes app-owned linking and non-originating delivery easier than the old host/channel model because
|
||||
app code already owns the native SDK clients, authenticated caller context, session id selection, and outbound sends.
|
||||
|
||||
### Future framework work
|
||||
|
||||
The following require a separate reviewed design before becoming reusable framework features:
|
||||
|
||||
- reusable cross-channel identity linking;
|
||||
- framework-owned proactive or non-originating delivery;
|
||||
- fan-out, multicast, selected-channel, active-channel, or all-linked delivery;
|
||||
- framework-owned delivery observability, dead-letter handling, and replay semantics;
|
||||
- cross-channel confidentiality and link policy.
|
||||
|
||||
[ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md) tracks possible follow-up work in this area and
|
||||
must be aligned with the helper-first model before implementation. Old vocabulary such as `IdentityLinker`,
|
||||
`ResponseTarget`, `ChannelPush`, `ChannelPushCodec`, `DurableTaskRunner`, `RetryPolicy`, and `LinkPolicy` is not v1 API.
|
||||
These are follow-up design topics, not hidden requirements of the v1 host.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Import surface | v1 helper-first contents |
|
||||
| Package | Import surface | Contents |
|
||||
|---|---|---|
|
||||
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
|
||||
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
|
||||
| Future protocol packages | e.g. `agent_framework_hosting_telegram` | Protocol-specific helpers such as `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_session_id(...)`, and command/media helpers when useful. |
|
||||
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentFrameworkHost`, channel protocols, key request/result types, hooks, `reset_session`, state-path helpers. |
|
||||
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | `ResponsesChannel`. |
|
||||
| `agent-framework-hosting-invocations` | `agent_framework_hosting_invocations` | `InvocationsChannel`. |
|
||||
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | `TelegramChannel` and Telegram command helpers. |
|
||||
| `agent-framework-hosting-activity-protocol` | `agent_framework_hosting_activity_protocol` | `ActivityProtocolChannel` for Activity Protocol over Azure Bot Service. |
|
||||
| `agent-framework-hosting-discord` | `agent_framework_hosting_discord` | `DiscordChannel` and Discord command/interaction helpers. |
|
||||
| `agent-framework-foundry-hosting` | `agent_framework.foundry_hosting` | Foundry isolation middleware and Foundry-backed hosting helpers usable with the v1 host. |
|
||||
|
||||
The core hosting package must not depend on protocol SDKs. Protocol packages may depend on their native protocol SDKs if
|
||||
needed, but helper functions should stay usable from plain app code and tests.
|
||||
Channel packages may depend on their native SDKs. The core hosting package should not depend on channel SDKs or on top-level legacy protocol hosts.
|
||||
|
||||
## Helper naming and families
|
||||
## Key Types
|
||||
|
||||
Helper names are protocol-specific. Avoid a generic `protocol_to_run(...)` public surface.
|
||||
### `AgentFrameworkHost`
|
||||
|
||||
Protocol packages may provide the following helper families when the protocol has the concept:
|
||||
The host constructor accepts:
|
||||
|
||||
| Helper family | Shape | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Run conversion | `<protocol>_to_run(...)` | Convert one protocol-native call/update/request into `Agent.run` or `Workflow.run` values. |
|
||||
| Final rendering | `<protocol>_from_run(...)` | Convert a final `AgentResponse` or workflow result into protocol-native response payloads or operations. |
|
||||
| Stream rendering | `<protocol>_from_streaming_run(...)` | Convert `ResponseStream` or workflow updates into protocol-native events or operations. |
|
||||
| Session id extraction | `<protocol>_session_id(...)` | Extract the protocol's natural continuation/partition key from the call, if present. |
|
||||
| Command/action parsing | `<protocol>_command(...)` | Parse a protocol-native command/action/operation name without deciding app policy. |
|
||||
- `target`: one `SupportsAgentRun`-compatible object or one `Workflow`
|
||||
- `channels`: one or more `Channel` instances
|
||||
- optional Starlette middleware
|
||||
- optional `state_dir`
|
||||
- optional workflow `checkpoint_location`
|
||||
|
||||
Examples:
|
||||
The host exposes:
|
||||
|
||||
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
|
||||
`responses_session_id(...)`;
|
||||
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
|
||||
`telegram_session_id(...)`, `telegram_command(...)`;
|
||||
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
|
||||
- `discord_to_run(...)`, `discord_from_run(...)`, `discord_session_id(...)`, `discord_command(...)`.
|
||||
- `app`: the canonical Starlette ASGI application
|
||||
- `serve(...)`: a convenience wrapper for local serving
|
||||
- `reset_session(isolation_key: str)`: rotate the cached `AgentSession` for a host-tracked conversation
|
||||
|
||||
This table is a naming guide, not a required checklist. A protocol package should add only the helpers that match native
|
||||
protocol concepts and current samples.
|
||||
`state_dir` is narrowed to v1 host-owned local files only:
|
||||
|
||||
Protocol-specific helpers may also exist for native details such as `telegram_chat_id(...)`,
|
||||
`telegram_callback_query_id(...)`, `telegram_media_file_id(...)`, `discord_interaction_id(...)`, `a2a_task_id(...)`,
|
||||
`a2a_context_id(...)`, or MCP tool/prompt/resource helpers. These helpers should stay side-effect-free. App/native SDK
|
||||
code performs acknowledgements, sends/edits messages, resolves protected file URLs, applies rate limits, and registers
|
||||
handlers.
|
||||
- session aliases (`isolation_key` to current `AgentSession` id), and
|
||||
- workflow checkpoint paths when the app chooses the host-provided file layout.
|
||||
|
||||
## `agent-framework-hosting` state helpers
|
||||
It is not a store for identity links, continuations, active-channel state, delivery attempts, or multicast payloads.
|
||||
|
||||
### `SessionStore`
|
||||
Externally supplied isolation keys are trusted only after the channel or host middleware has authenticated and authorized the caller. The host uses `isolation_key` as a partition key; the string itself is not proof of identity or ownership.
|
||||
|
||||
`SessionStore` is an in-memory async lookup:
|
||||
### `Channel`
|
||||
|
||||
A channel implements a small protocol:
|
||||
|
||||
- declare a stable channel id/name,
|
||||
- contribute routes, middleware, commands, and lifecycle callbacks,
|
||||
- parse inbound protocol data into `ChannelRequest`,
|
||||
- call the host through `ChannelContext.run(...)` or `ChannelContext.run_stream(...)`, and
|
||||
- serialize the returned result to the originating protocol response.
|
||||
|
||||
Channels own protocol authentication, signature validation, native command registration, and protocol-specific error bodies.
|
||||
|
||||
### `ChannelContribution`
|
||||
|
||||
`ChannelContribution` is the channel's host-facing contribution:
|
||||
|
||||
- Starlette routes and optional middleware,
|
||||
- native command descriptors,
|
||||
- startup and shutdown callbacks, and
|
||||
- any channel-local metadata needed by the package.
|
||||
|
||||
The host aggregates contributions but does not interpret protocol payloads.
|
||||
|
||||
### `ChannelRequest`
|
||||
|
||||
`ChannelRequest` is the host-neutral request envelope produced by a channel. It carries:
|
||||
|
||||
- target input,
|
||||
- optional `ChannelSession`,
|
||||
- optional `ChannelIdentity`,
|
||||
- options and attributes produced by the channel, and
|
||||
- request metadata useful to hooks and context providers.
|
||||
|
||||
The host may pass attributes through to context providers and middleware. Channels should treat attributes as a documented extension bag, not as a cross-channel delivery contract.
|
||||
|
||||
### `ChannelSession`
|
||||
|
||||
`ChannelSession(isolation_key=...)` is the only v1 session-continuity mechanism.
|
||||
|
||||
When a request contains an isolation key:
|
||||
|
||||
1. The host looks up or creates the cached `AgentSession` for that key.
|
||||
2. The target runs with that `AgentSession` when the target is an agent.
|
||||
3. `reset_session(isolation_key)` rotates the alias so the next request starts a new conversation.
|
||||
|
||||
If two channels produce the same isolation key on the same host, they share the same cached session. If they produce different keys, they do not share session state.
|
||||
|
||||
### `ChannelIdentity`
|
||||
|
||||
`ChannelIdentity` is optional request metadata such as channel id, native user id, tenant id, claims, or display attributes.
|
||||
|
||||
In v1, `ChannelIdentity` does not link channels, authorize callers, select delivery destinations, or imply that two identities should share an `AgentSession`. A channel that wants shared history must still produce the same `ChannelSession.isolation_key`.
|
||||
|
||||
### Hooks
|
||||
|
||||
Hooks are optional and channel-owned:
|
||||
|
||||
- `ChannelRunHook`: runs after channel parsing and before host invocation; returns the `ChannelRequest` to execute.
|
||||
- `ChannelResponseHook`: runs after target completion and before the originating channel renders a one-shot response.
|
||||
- `ChannelStreamUpdateHook`: the host applies it to streamed updates before the originating channel serializes the stream.
|
||||
|
||||
Common uses include adapting chat text into workflow inputs, enforcing deployment-specific options, flattening rich output for text-only protocols, or filtering streamed updates for a protocol. Stream update hooks are update-only; they do not automatically sanitize `get_final_response()` output. Channels choose their response transport from the parsed protocol request before invoking run hooks.
|
||||
|
||||
### `HostedRunResult`
|
||||
|
||||
`HostedRunResult[T]` wraps the target's full-fidelity result plus the resolved `AgentSession | None`.
|
||||
|
||||
- Agent targets produce `HostedRunResult[AgentResponse]`.
|
||||
- Workflow targets produce `HostedRunResult[WorkflowRunResult]`.
|
||||
|
||||
The host does not flatten, filter, or translate the result. Each channel decides how much of the result its protocol can carry.
|
||||
|
||||
## Host Behavior
|
||||
|
||||
1. `AgentFrameworkHost` builds one Starlette app and asks each channel for its contribution.
|
||||
2. A channel route receives a protocol-native request.
|
||||
3. The channel validates/parses the native payload and creates `ChannelRequest`.
|
||||
4. The channel passes the request, optional `ChannelRunHook`, and protocol-native context to the host.
|
||||
5. The host invokes `ChannelRunHook`, if configured, and receives the prepared request.
|
||||
6. The host resolves an `AgentSession` from `ChannelSession.isolation_key` when present.
|
||||
7. The host invokes the agent or workflow target.
|
||||
8. The host wraps the result in `HostedRunResult` or the streaming equivalent.
|
||||
9. The host invokes `ChannelResponseHook`, if configured, for non-streaming/final response shaping.
|
||||
10. The host applies stream update hooks while the channel consumes streams; the channel renders the originating protocol response.
|
||||
|
||||
There is no host-level route from one channel's request to another channel's response in v1.
|
||||
|
||||
## Workflow Checkpoints
|
||||
|
||||
Workflow checkpointing is explicit. Apps either configure checkpoint storage on the workflow itself or pass a `checkpoint_location` to the host so the workflow dispatch path can use the intended file location.
|
||||
|
||||
`state_dir` may provide a conventional location for workflow checkpoint files, but checkpointing is still opt-in and separate from agent session history. Checkpoints are workflow-runtime state, not channel state and not identity-link state.
|
||||
|
||||
## Foundry Isolation Middleware
|
||||
|
||||
V1 keeps Foundry isolation as middleware rather than as a channel-linking feature.
|
||||
|
||||
The middleware is installed only when the Foundry hosting environment flag is present. In that environment it reads Foundry-provided isolation values at the trusted hosting boundary, exposes them as read-only request context for Foundry-aware history or memory providers, and rejects unsafe session resumes when the live isolation context does not match persisted session context. Outside Foundry, raw isolation headers are ignored unless an app supplies its own trusted middleware.
|
||||
|
||||
This middleware does not create cross-channel identity links and does not authorize non-Foundry channels.
|
||||
|
||||
## Current Channels
|
||||
|
||||
### Responses
|
||||
|
||||
`ResponsesChannel` exposes the OpenAI-compatible Responses API shape. It maps request body fields such as input, options, and conversation identifiers into `ChannelRequest`, and it renders Responses-compatible one-shot or streaming responses.
|
||||
|
||||
Responses session continuity uses a channel-selected `isolation_key`, commonly derived from a response/conversation id, caller-provided session id, Foundry isolation context, or deployment-specific request metadata.
|
||||
|
||||
### Invocations
|
||||
|
||||
`InvocationsChannel` exposes an invocation endpoint for server-side callers and tools. It maps the request body into `ChannelRequest` and renders the invocation result on the same HTTP response.
|
||||
|
||||
Invocations is useful for typed workflow inputs because a `ChannelRunHook` can translate the request body into the workflow's expected input type.
|
||||
|
||||
### Telegram
|
||||
|
||||
`TelegramChannel` supports webhook or polling transport, native command registration, and message rendering back to the originating Telegram chat.
|
||||
|
||||
The channel chooses a default `isolation_key` from Telegram-native data such as chat id, user id, or a configured user/chat scope. A `/new` or equivalent command may call `reset_session` for that isolation key.
|
||||
|
||||
### Activity Protocol
|
||||
|
||||
`ActivityChannel` supports Activity Protocol requests, typically through Azure Bot Service for Teams, Web Chat, and other Bot Framework-fronted surfaces.
|
||||
|
||||
The channel maps incoming `Activity` objects to `ChannelRequest` and renders a reply activity to the originating conversation. Proactive Activity delivery, active-channel routing, and all-linked fan-out are not v1 host semantics.
|
||||
|
||||
### Discord
|
||||
|
||||
`DiscordChannel` supports Discord messages, slash commands, and interactions as channel-native input.
|
||||
|
||||
The channel maps Discord-native user, guild, channel, thread, and interaction data into `ChannelRequest` metadata and a configured `ChannelSession.isolation_key`. It renders the result to the originating Discord response path.
|
||||
|
||||
## High-level Samples
|
||||
|
||||
### One agent on Responses
|
||||
|
||||
```python
|
||||
class SessionStore:
|
||||
async def get(self, session_id: str) -> AgentSession | None: ...
|
||||
async def set(self, session_id: str, session: AgentSession) -> None: ...
|
||||
async def delete(self, session_id: str) -> None: ...
|
||||
```
|
||||
|
||||
The store does not create sessions. It stores `session_id -> AgentSession` values supplied by callers.
|
||||
|
||||
The built-in store has no TTL or eviction. This is intentional for local/dev and simple process-local scenarios: protocols
|
||||
such as OpenAI Responses can continue from any prior response id. Durable or multi-replica deployments should provide a
|
||||
durable store and their own TTL/eviction policy.
|
||||
|
||||
### `AgentState`
|
||||
|
||||
`AgentState` holds an agent target and an optional `SessionStore`:
|
||||
|
||||
```python
|
||||
state = AgentState(agent)
|
||||
state = AgentState(create_agent)
|
||||
state = AgentState(create_agent, cache_target=False)
|
||||
```
|
||||
|
||||
The target may be:
|
||||
|
||||
- a `SupportsAgentRun` instance;
|
||||
- a synchronous factory;
|
||||
- an asynchronous factory;
|
||||
- an awaitable target.
|
||||
|
||||
`AgentState` provides:
|
||||
|
||||
- `await get_target()`;
|
||||
- synchronous `target` only after a target is already available/resolved;
|
||||
- `session_store`;
|
||||
- `await get_or_create_session(session_id)`;
|
||||
- `await set_session(session_id, session)`.
|
||||
|
||||
`get_or_create_session(...)` resolves the target and calls `target.create_session(session_id=...)` only when the store has
|
||||
no session for that id.
|
||||
|
||||
Apps must store the post-run session explicitly after `agent.run(...)` or stream finalization:
|
||||
|
||||
```python
|
||||
session = await state.get_or_create_session(session_id)
|
||||
target = await state.get_target()
|
||||
result = await target.run(messages, session=session, options=options)
|
||||
await state.set_session(response_id, session)
|
||||
```
|
||||
|
||||
### `WorkflowState`
|
||||
|
||||
`WorkflowState` resolves a workflow target. It does not own checkpoint storage.
|
||||
|
||||
The target may be:
|
||||
|
||||
- a `Workflow` instance;
|
||||
- a `WorkflowBuilder` or other object with `build() -> Workflow`;
|
||||
- a synchronous factory;
|
||||
- an asynchronous factory;
|
||||
- an awaitable target.
|
||||
|
||||
`WorkflowState` provides:
|
||||
|
||||
- `await get_target()`;
|
||||
- synchronous `target` only after a target is already available/resolved.
|
||||
|
||||
Workflow checkpointing uses Agent Framework's existing `CheckpointStorage` abstraction directly. Apps that need
|
||||
per-session workflow resume should keep an app-owned cursor such as `session_id -> checkpoint_id`. When the app uses
|
||||
file-backed cursor storage, the file-based checkpoint storage should share the same app storage root and should be
|
||||
scoped to the current authenticated user/tenant/session bucket, for example
|
||||
`storage/checkpoints/<session-bucket>/` beside `storage/checkpoint_cursors.json`:
|
||||
|
||||
```python
|
||||
# session_id must already be authenticated and authorized for this caller
|
||||
target = await workflow_state.get_target()
|
||||
checkpoint_id = await checkpoint_cursor_store.get(session_id)
|
||||
if checkpoint_id is None:
|
||||
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
|
||||
else:
|
||||
result = await target.run(checkpoint_id=checkpoint_id, checkpoint_storage=checkpoint_storage)
|
||||
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
|
||||
if latest is not None:
|
||||
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
|
||||
```
|
||||
|
||||
`Workflow.run(...)` does not currently emit a checkpoint id on `WorkflowRunResult` or normal workflow events by default.
|
||||
The runner receives checkpoint ids internally from `CheckpointStorage.save(...)`. Apps that own the storage can query
|
||||
`get_latest(workflow_name=...)` after the run if they need to update a cursor.
|
||||
|
||||
## `agent-framework-hosting-responses`
|
||||
|
||||
The Responses package provides the helper-first surface for OpenAI Responses-shaped requests.
|
||||
|
||||
### Request helpers
|
||||
|
||||
- `messages_from_responses_input(input) -> list[Message]`
|
||||
- `responses_to_run(body) -> AgentRunArgs`
|
||||
- `responses_session_id(body) -> str | None`
|
||||
- `create_response_id() -> str`
|
||||
|
||||
`responses_to_run(...)` returns values corresponding to `Agent.run(...)`:
|
||||
|
||||
```python
|
||||
run = responses_to_run(body)
|
||||
messages = run["messages"]
|
||||
options = run["options"]
|
||||
stream = run["stream"]
|
||||
```
|
||||
|
||||
It excludes protocol transport/session fields from `options` and remaps known Responses option names such as
|
||||
`max_output_tokens -> max_tokens`.
|
||||
|
||||
`responses_session_id(...)` returns:
|
||||
|
||||
- `previous_response_id` when present (`resp_*`);
|
||||
- otherwise `conversation_id` when present (`conv_*`);
|
||||
- otherwise `None`.
|
||||
|
||||
The helper only extracts the candidate key. App code decides whether to trust and use that key.
|
||||
|
||||
### Response helpers
|
||||
|
||||
- `responses_from_run(result, *, response_id, session_id=None) -> dict[str, Any]`
|
||||
- `responses_from_streaming_run(stream, *, response_id, session_id=None) -> AsyncIterator[str]`
|
||||
|
||||
`responses_from_run(...)` renders a full Responses JSON payload from an `AgentResponse`. It renders the full set of
|
||||
OpenAI Responses output item types supported by Agent Framework content.
|
||||
|
||||
`responses_from_streaming_run(...)` renders Server-Sent Event strings for a `ResponseStream`. It emits a created event,
|
||||
text deltas, and a completed event. The final completed payload is produced through `responses_from_run(...)`; the helper
|
||||
also preserves the model id observed on streaming updates when the finalized `AgentResponse` no longer carries raw model
|
||||
metadata.
|
||||
|
||||
## Security responsibilities
|
||||
|
||||
Protocol helper packages parse and render. They do not authenticate callers, authorize access to state, or decide which
|
||||
side effects are allowed.
|
||||
|
||||
Application code that uses these helpers is responsible for:
|
||||
|
||||
- authenticating the caller through the app's normal mechanism before using protocol-provided ids;
|
||||
- authorizing any caller-supplied session, checkpoint, task, context, conversation, thread, or response id before loading
|
||||
state for it;
|
||||
- binding externally supplied ids to the authenticated user, tenant, workspace, installation, or chat context before
|
||||
using them as `SessionStore` keys or checkpoint cursor keys;
|
||||
- treating `<protocol>_session_id(...)` results as untrusted candidate keys until that ownership check has passed;
|
||||
- keeping platform-provided isolation helpers fail-closed outside their trusted hosting environment;
|
||||
- authorizing command/action effects such as reset, cancel, approve, submit, or tool invocation after parsing them;
|
||||
- opting in explicitly before resolving protected media/resource/file URLs and passing them to a remote model provider;
|
||||
- persisting post-run session or checkpoint state only after `agent.run(...)`, `workflow.run(...)`, or stream finalization
|
||||
has updated that state.
|
||||
|
||||
## Persistent versus transient hosting
|
||||
|
||||
The application builder decides whether the server is persistent or transient.
|
||||
|
||||
- Persistent single-process apps, such as a long-running container or web app, may use in-memory state for local
|
||||
development or simple deployments. Multi-replica persistent apps still need durable state for continuity.
|
||||
- Transient apps, such as Azure Functions, Foundry Hosted Agents, or any environment where process memory is not a
|
||||
reliable boundary, must not rely on in-memory `SessionStore` state between calls. They need a durable session store or
|
||||
a service-owned continuation id.
|
||||
- Workflow hosts must choose an explicit `CheckpointStorage` and, when they need per-session resume, a durable
|
||||
`session_id -> checkpoint_id` cursor. File-backed checkpoint storage and file-backed cursor storage should live under
|
||||
the same app storage root, with checkpoints scoped to the current authenticated user/tenant/session bucket so a
|
||||
"latest checkpoint" lookup cannot cross conversations. In-process workflow state and in-memory checkpoint cursors do
|
||||
not survive transient execution.
|
||||
|
||||
## Minimal FastAPI Responses shape
|
||||
|
||||
This is the shape the local Responses sample should demonstrate. It is not an app framework.
|
||||
|
||||
```python
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from agent_framework import ResponseStream
|
||||
from agent_framework_hosting import AgentState
|
||||
from agent_framework_hosting_responses import (
|
||||
create_response_id,
|
||||
responses_from_run,
|
||||
responses_from_streaming_run,
|
||||
responses_session_id,
|
||||
responses_to_run,
|
||||
host = AgentFrameworkHost(
|
||||
target=agent,
|
||||
channels=[ResponsesChannel()],
|
||||
)
|
||||
from fastapi import Body, FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
app = FastAPI()
|
||||
state = AgentState(create_agent)
|
||||
|
||||
|
||||
@app.post("/responses", response_model=None)
|
||||
async def responses(body: dict = Body(...)) -> JSONResponse | StreamingResponse:
|
||||
run = responses_to_run(body)
|
||||
candidate_session_id = responses_session_id(body)
|
||||
response_id = create_response_id()
|
||||
|
||||
# Verify this caller owns candidate_session_id before loading it.
|
||||
session_id = candidate_session_id or response_id
|
||||
session = await state.get_or_create_session(session_id)
|
||||
target = await state.get_target()
|
||||
|
||||
if run["stream"]:
|
||||
stream = target.run(run["messages"], stream=True, session=session, options=run["options"])
|
||||
if not isinstance(stream, ResponseStream):
|
||||
raise HTTPException(status_code=500, detail="agent did not return a response stream")
|
||||
|
||||
async def events() -> AsyncIterator[str]:
|
||||
async for event in responses_from_streaming_run(
|
||||
stream,
|
||||
response_id=response_id,
|
||||
session_id=candidate_session_id,
|
||||
):
|
||||
yield event
|
||||
await state.set_session(response_id, session)
|
||||
|
||||
return StreamingResponse(events(), media_type="text/event-stream")
|
||||
|
||||
result = await target.run(run["messages"], session=session, options=run["options"])
|
||||
await state.set_session(response_id, session)
|
||||
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
|
||||
app = host.app
|
||||
```
|
||||
|
||||
## Validation
|
||||
### One agent on multiple channels
|
||||
|
||||
Implementation validation must cover:
|
||||
```python
|
||||
host = AgentFrameworkHost(
|
||||
target=agent,
|
||||
channels=[
|
||||
ResponsesChannel(),
|
||||
InvocationsChannel(),
|
||||
TelegramChannel(bot_token=os.environ["TELEGRAM_BOT_TOKEN"]),
|
||||
],
|
||||
)
|
||||
|
||||
- `SessionStore` plain get/set/delete behavior;
|
||||
- `AgentState` target resolution, target caching, and get-or-create session behavior;
|
||||
- `WorkflowState` target resolution for direct workflows, factories, `WorkflowBuilder`, and orchestration-style builders;
|
||||
- Responses request parsing and option remapping;
|
||||
- Responses session id extraction;
|
||||
- Responses response rendering, including rich output item mapping;
|
||||
- Responses streaming SSE rendering;
|
||||
- HTTP round-trip tests showing a native FastAPI route using `AgentState` and Responses helpers;
|
||||
- sample type checking for the local Responses sample.
|
||||
host.serve(host="localhost", port=8000)
|
||||
```
|
||||
|
||||
The host owns one Starlette app. Each channel contributes its own routes and renders its own response.
|
||||
|
||||
### Adapting a request before execution
|
||||
|
||||
```python
|
||||
from dataclasses import replace
|
||||
|
||||
|
||||
def enforce_options(request: ChannelRequest) -> ChannelRequest:
|
||||
options = dict(request.options or {})
|
||||
options["temperature"] = 0
|
||||
return replace(request, options=options)
|
||||
|
||||
|
||||
host = AgentFrameworkHost(
|
||||
target=agent,
|
||||
channels=[ResponsesChannel(run_hook=enforce_options)],
|
||||
)
|
||||
```
|
||||
|
||||
### Workflow with explicit checkpoints
|
||||
|
||||
```python
|
||||
host = AgentFrameworkHost(
|
||||
target=workflow,
|
||||
channels=[InvocationsChannel(run_hook=adapt_to_workflow_input)],
|
||||
checkpoint_location=Path("./.af-hosting/workflow_checkpoints"),
|
||||
)
|
||||
```
|
||||
|
||||
The hook adapts channel-native input to the workflow's typed input. Checkpoints use the explicit workflow checkpoint location, not identity-link or delivery storage.
|
||||
|
||||
### Message channel reset command
|
||||
|
||||
```python
|
||||
async def new_chat(context):
|
||||
if context.request.session is not None:
|
||||
await context.host.reset_session(context.request.session.isolation_key)
|
||||
await context.reply("Started a new conversation.")
|
||||
```
|
||||
|
||||
Telegram, Activity Protocol, and Discord can expose equivalent native commands when their protocols support them.
|
||||
|
||||
## Follow-up Enhancements
|
||||
|
||||
See [ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md) for the deferred design covering:
|
||||
|
||||
- cross-channel identity linking,
|
||||
- authorization and allowlists,
|
||||
- non-originating response delivery,
|
||||
- active-channel routing,
|
||||
- multicast and all-linked delivery,
|
||||
- background runs and continuation tokens,
|
||||
- durable delivery runners,
|
||||
- retry/replay semantics, and
|
||||
- payload serialization.
|
||||
|
||||
Those enhancements must layer on top of this v1 contract without requiring v1 users to adopt them.
|
||||
|
||||
## Validation Gates
|
||||
|
||||
The Python implementation should be considered complete when:
|
||||
|
||||
- a sample uses one `AgentFrameworkHost` with multiple channels and no manual Starlette route composition,
|
||||
- each current channel has contract tests for route contribution, lifecycle, request parsing, hooks, and originating response rendering,
|
||||
- session tests prove shared `isolation_key` values share an `AgentSession` and `reset_session` rotates it,
|
||||
- workflow tests or samples use explicit `checkpoint_location`,
|
||||
- Foundry isolation middleware is covered by integration or contract tests,
|
||||
- no v1 package exposes the removed linking, multicast, durable-runner, or continuation APIs, and
|
||||
- this spec and ADR-0027 remain aligned.
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../.github/skills/pull-requests
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
---
|
||||
name: pull-requests
|
||||
description: >
|
||||
Guidance for creating pull requests and handling PR review comments in the
|
||||
Agent Framework repository. Use this when writing a PR description (filling out
|
||||
the PR template) or when responding to and resolving review comments on an
|
||||
existing PR.
|
||||
---
|
||||
|
||||
# Pull Request Workflow
|
||||
|
||||
This skill covers two tasks: (1) writing a high-quality PR description, and
|
||||
(2) handling review comments on an existing PR.
|
||||
|
||||
## 1. Writing the PR description
|
||||
|
||||
Always follow the repository PR template at
|
||||
[`.github/pull_request_template.md`](../../../../.github/pull_request_template.md). Keep its
|
||||
exact structure and headings. Fill every section:
|
||||
|
||||
### `### Motivation & Context`
|
||||
Explain *why* the change is needed: the problem it solves and the scenario it
|
||||
contributes to. Describe the net change relative to `main` — this is implied, so
|
||||
do **not** spell out "vs main" explicitly.
|
||||
|
||||
### `### Description & Review Guide`
|
||||
Describe the changes, the overall approach, and the design. Answer the three
|
||||
prompts:
|
||||
- **What are the major changes?**
|
||||
- **What is the impact of these changes?**
|
||||
- **What do you want reviewers to focus on?** — This item is for **human
|
||||
reviewers only**. Automated/AI reviewers must ignore it and review the entire
|
||||
change rather than narrowing scope to it.
|
||||
|
||||
### `### Related Issue`
|
||||
Link the issue the PR fixes using a GitHub closing keyword (`Fixes #123` /
|
||||
`Closes #123`) so it closes automatically on merge. A PR with no linked issue may
|
||||
be closed regardless of how valid the change is. Before opening, confirm there is
|
||||
no other open PR for the same issue; if there is, explain how this PR differs.
|
||||
|
||||
### `### Contribution Checklist`
|
||||
Check every item that applies. For the breaking-change item:
|
||||
- Leave **"This is not a breaking change."** checked for the common case.
|
||||
- If the change **is** breaking, add the `breaking change` label **or** put
|
||||
`[BREAKING]` in the title prefix, before or after a language prefix such as
|
||||
`Python:` or `.NET:` — workflows keep the label and the title prefix in sync
|
||||
automatically (see `.github/workflows/label-title-prefix.yml` and
|
||||
`.github/workflows/label-pr.yml`).
|
||||
|
||||
### Do not
|
||||
- Do **not** add ad-hoc sections such as "Validation" or "Tests run"; CI/CD and
|
||||
the checklist already cover validation status.
|
||||
- Do **not** remove or reorder the template's headings.
|
||||
|
||||
### Creating the PR
|
||||
Open new PRs as **drafts** until they are ready for review. Example:
|
||||
|
||||
```bash
|
||||
gh pr create --repo microsoft/agent-framework --base main \
|
||||
--head <your-fork-owner>:<branch> --draft \
|
||||
--title "<concise title>" --body "<body following the template>"
|
||||
```
|
||||
|
||||
## 2. Handling review comments
|
||||
|
||||
When a PR receives review comments, follow this sequence — **do not start editing
|
||||
code before the user has reviewed the plan**:
|
||||
|
||||
1. **Review the comments.** Read every review comment and thread on the PR,
|
||||
including inline code comments and general review summaries.
|
||||
2. **Make a plan.** Produce a concrete plan describing how each comment will be
|
||||
addressed (or why it should not be, with reasoning).
|
||||
3. **Let the user review the plan.** Present the plan and wait for the user's
|
||||
approval or adjustments before implementing anything.
|
||||
4. **Implement.** Make the agreed changes.
|
||||
5. **Reply to every comment.** Add a reply to **all** comments explaining how it
|
||||
was addressed (or the agreed outcome) — leave none unanswered.
|
||||
6. **Resolve resolved threads.** Mark a review thread as resolved only when the
|
||||
comment has actually been addressed.
|
||||
|
||||
### Useful commands
|
||||
|
||||
List review comments and threads:
|
||||
|
||||
```bash
|
||||
# Inline review comments
|
||||
gh api repos/{owner}/{repo}/pulls/{pr}/comments
|
||||
|
||||
# Review threads with resolution state (GraphQL)
|
||||
gh api graphql -f query='
|
||||
query($owner:String!,$repo:String!,$pr:Int!){
|
||||
repository(owner:$owner,name:$repo){
|
||||
pullRequest(number:$pr){
|
||||
reviewThreads(first:100){
|
||||
nodes{ id isResolved comments(first:50){ nodes{ id body author{login} } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -F owner={owner} -F repo={repo} -F pr={pr}
|
||||
```
|
||||
|
||||
Reply to an inline review comment:
|
||||
|
||||
```bash
|
||||
gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies \
|
||||
-f body="Addressed in <commit>: <explanation>"
|
||||
```
|
||||
|
||||
Resolve a review thread (needs the thread node id from the GraphQL query above):
|
||||
|
||||
```bash
|
||||
gh api graphql -f query='
|
||||
mutation($threadId:ID!){
|
||||
resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
|
||||
}' -F threadId={thread_id}
|
||||
```
|
||||
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.31.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.20.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.6.0" />
|
||||
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
@@ -23,14 +23,14 @@
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.7" /> <!-- Transitive dependency of Aspire pinned to newer version due to vulnerability in 2.5.192 -->
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.26" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.25" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.4" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.5" />
|
||||
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.3" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Core" Version="1.60.0" />
|
||||
<PackageVersion Include="Azure.Core" Version="1.57.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
|
||||
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
|
||||
@@ -42,24 +42,18 @@
|
||||
<!-- Newtonsoft.Json -->
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.14.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.13.0" />
|
||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.9" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.8" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.8" />
|
||||
<!-- AG-UI .NET SDK packages (published by the AG-UI team). -->
|
||||
<PackageVersion Include="AGUI.Abstractions" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Formatting" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Protobuf" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Client" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Server" Version="0.0.3" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.8" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.8" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
|
||||
@@ -76,8 +70,7 @@
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="2.7.5" /> <!-- Pin patched OpenAPI.NET to remediate GHSA-v5pm-xwqc-g5wc -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.6.0" />
|
||||
@@ -94,12 +87,12 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
|
||||
@@ -107,7 +100,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.5" />
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
@@ -127,7 +120,7 @@
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.10.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.84.2" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
|
||||
<!-- Workflows -->
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.4.1" />
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Solution>
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<BuildType Name="Debug" />
|
||||
<BuildType Name="Publish" />
|
||||
@@ -117,13 +117,10 @@
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills/Agent_Step06_McpBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/Harness/">
|
||||
<File Path="samples/02-agents/Harness/README.md" />
|
||||
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step01_MeetYourClaw/Claw_Step01_MeetYourClaw.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Claw_Step03_ScalingCapabilities.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
|
||||
@@ -196,10 +193,10 @@
|
||||
<File Path="samples/02-agents/AgentWithMemory/README.md" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory/AgentWithMemory_Step05_BoundedChatHistory.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
|
||||
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
|
||||
@@ -244,10 +241,10 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Declarative/">
|
||||
<File Path="samples/03-workflows/Declarative/README.md" />
|
||||
<Project Path="samples/03-workflows/Declarative/AotCheckpointing/AotCheckpointing.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||
@@ -370,7 +367,6 @@
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/Hosted-Toolbox-AuthPaths-Client/Hosted-Toolbox-AuthPaths-Client.csproj" />
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj" />
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj" />
|
||||
</Folder>
|
||||
@@ -606,6 +602,7 @@
|
||||
<Project Path="src/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
@@ -620,9 +617,9 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj" />
|
||||
@@ -631,13 +628,13 @@
|
||||
<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.Tools.Shell/Microsoft.Agents.AI.Tools.Shell.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Tests/" />
|
||||
<Folder Name="/Tests/IntegrationTests/">
|
||||
@@ -664,6 +661,7 @@
|
||||
<Project Path="tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
@@ -687,11 +685,11 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/Microsoft.Agents.AI.Tools.Shell.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"projects": [
|
||||
"src\\Microsoft.Agents.AI.A2A\\Microsoft.Agents.AI.A2A.csproj",
|
||||
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
|
||||
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
|
||||
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
|
||||
"src\\Microsoft.Agents.AI.Harness\\Microsoft.Agents.AI.Harness.csproj",
|
||||
|
||||
@@ -335,8 +335,8 @@ internal static class AgentsSamples
|
||||
{
|
||||
Name = "Agent_Step01_FileBasedSkills",
|
||||
ProjectPath = "samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"Converting units with file-based skills",
|
||||
@@ -354,8 +354,8 @@ internal static class AgentsSamples
|
||||
{
|
||||
Name = "Agent_Step06_McpBasedSkills",
|
||||
ProjectPath = "samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"Discovering MCP-based skills",
|
||||
@@ -701,7 +701,7 @@ internal static class AgentsSamples
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain the current EUR exchange rate against USD and GBP as numeric values.",
|
||||
"The output should contain a list of countries or information about countries that use the EUR currency.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
@@ -1052,8 +1052,8 @@ internal static class AgentsSamples
|
||||
{
|
||||
Name = "FoundryAgent_Step15_ComputerUse",
|
||||
ProjectPath = "samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "AZURE_AI_COMPUTER_USE_DEPLOYMENT_NAME"],
|
||||
OptionalEnvironmentVariables = [],
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription = ["The output should show a computer automation session processing simulated browser screenshots with iteration steps and a final response describing search results."],
|
||||
},
|
||||
|
||||
|
||||
@@ -18,14 +18,13 @@
|
||||
// Note: By default, this tool expects sample build outputs to already exist.
|
||||
// Pre-build the solution before running, or pass --build to avoid missing build output failures.
|
||||
//
|
||||
// Required environment variables (for AI-powered verification):
|
||||
// FOUNDRY_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint
|
||||
// FOUNDRY_MODEL — Model deployment name (optional, defaults to gpt-5.4-mini)
|
||||
// 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.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using VerifySamples;
|
||||
|
||||
var options = VerifyOptions.Parse(args);
|
||||
@@ -44,33 +43,14 @@ if (!File.Exists(Path.Combine(dotnetRoot, "agent-framework-dotnet.slnx")))
|
||||
}
|
||||
|
||||
// Set up the AI verifier
|
||||
var foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
|
||||
var foundryModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5-mini";
|
||||
|
||||
AIAgent? verifierAgent = null;
|
||||
if (!string.IsNullOrEmpty(foundryEndpoint))
|
||||
OpenAI.Chat.ChatClient? chatClient = null;
|
||||
if (!string.IsNullOrEmpty(endpoint))
|
||||
{
|
||||
verifierAgent = new AIProjectClient(new Uri(foundryEndpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(
|
||||
model: foundryModel,
|
||||
instructions: """
|
||||
You are a test output verifier. You will be given:
|
||||
1. The actual stdout output of a program
|
||||
2. The stderr output (if any)
|
||||
3. 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.
|
||||
|
||||
In your response, you MUST:
|
||||
- Always provide ai_reasoning with a brief overall assessment.
|
||||
- Always provide exactly one entry in expectation_results for each expectation,
|
||||
in the same order as the input list.
|
||||
- For each expectation_results entry, echo the expectation text in the expectation
|
||||
field and explain your assessment in the detail field, citing evidence from the output.
|
||||
""",
|
||||
name: "OutputVerifier");
|
||||
chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
}
|
||||
|
||||
// Set up optional log file writer
|
||||
@@ -81,13 +61,11 @@ if (options.LogFilePath is not null)
|
||||
await logWriter.WriteHeaderAsync();
|
||||
}
|
||||
|
||||
Console.WriteLine($"Foundry endpoint: {foundryEndpoint ?? "(not set — AI verification disabled)"}, Model: {foundryModel}");
|
||||
|
||||
try
|
||||
{
|
||||
// Run all samples
|
||||
var reporter = new ConsoleReporter();
|
||||
var verifier = new SampleVerifier(verifierAgent);
|
||||
var verifier = new SampleVerifier(chatClient);
|
||||
var orchestrator = new VerificationOrchestrator(verifier, reporter, dotnetRoot, TimeSpan.FromMinutes(3), logWriter, buildSamples: options.BuildSamples);
|
||||
|
||||
var run = await orchestrator.RunAllAsync(options.Samples, options.MaxParallelism);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
namespace VerifySamples;
|
||||
|
||||
@@ -15,12 +17,33 @@ internal sealed class SampleVerifier
|
||||
private readonly AIAgent? _verifierAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a verifier. If <paramref name="verifierAgent"/> is provided,
|
||||
/// Creates a verifier. If <paramref name="chatClient"/> is provided,
|
||||
/// AI-based verification is available for non-deterministic samples.
|
||||
/// </summary>
|
||||
public SampleVerifier(AIAgent? verifierAgent = null)
|
||||
public SampleVerifier(ChatClient? chatClient = null)
|
||||
{
|
||||
this._verifierAgent = verifierAgent;
|
||||
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. The stderr output (if any)
|
||||
3. 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.
|
||||
|
||||
In your response, you MUST:
|
||||
- Always provide ai_reasoning with a brief overall assessment.
|
||||
- Always provide exactly one entry in expectation_results for each expectation,
|
||||
in the same order as the input list.
|
||||
- For each expectation_results entry, echo the expectation text in the expectation
|
||||
field and explain your assessment in the detail field, citing evidence from the output.
|
||||
""",
|
||||
name: "OutputVerifier");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,8 +30,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_StartHere_02_AgentsInWorkflows",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/02_AgentsInWorkflows",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show agent responses from a translation workflow.",
|
||||
@@ -43,8 +43,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_StartHere_03_AgentWorkflowPatterns",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/03_AgentWorkflowPatterns",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
Inputs = ["sequential"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription =
|
||||
@@ -81,8 +81,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_StartHere_06_MixedWorkflowAgentsAndExecutors",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
Inputs = ["What is 2 plus 2?"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription =
|
||||
@@ -96,8 +96,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_StartHere_07_WriterCriticWorkflow",
|
||||
ProjectPath = "samples/03-workflows/_StartHere/07_WriterCriticWorkflow",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain = ["=== Writer-Critic Iteration Workflow ==="],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
@@ -115,8 +115,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Agents_CustomAgentExecutors",
|
||||
ProjectPath = "samples/03-workflows/Agents/CustomAgentExecutors",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show custom workflow events including slogan generation and feedback.",
|
||||
@@ -128,8 +128,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Agents_FoundryAgent",
|
||||
ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Requires Azure AI Foundry project endpoint.",
|
||||
},
|
||||
|
||||
@@ -137,8 +137,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Agents_GroupChatToolApproval",
|
||||
ProjectPath = "samples/03-workflows/Agents/GroupChatToolApproval",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain = ["Starting group chat workflow for software deployment..."],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
@@ -153,8 +153,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Agents_WorkflowAsAnAgent",
|
||||
ProjectPath = "samples/03-workflows/Agents/WorkflowAsAnAgent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
Inputs = ["hello", "exit"],
|
||||
InputDelayMs = 5000,
|
||||
ExpectedOutputDescription =
|
||||
@@ -219,8 +219,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Concurrent_Concurrent",
|
||||
ProjectPath = "samples/03-workflows/Concurrent/Concurrent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show results from concurrent agent processing.",
|
||||
@@ -247,8 +247,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_ConditionalEdges_01_EdgeCondition",
|
||||
ProjectPath = "samples/03-workflows/ConditionalEdges/01_EdgeCondition",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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.",
|
||||
@@ -260,8 +260,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_ConditionalEdges_02_SwitchCase",
|
||||
ProjectPath = "samples/03-workflows/ConditionalEdges/02_SwitchCase",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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.",
|
||||
@@ -273,8 +273,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_ConditionalEdges_03_MultiSelection",
|
||||
ProjectPath = "samples/03-workflows/ConditionalEdges/03_MultiSelection",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an email being classified and potentially routed to multiple handlers.",
|
||||
@@ -371,8 +371,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Observability_WorkflowAsAnAgent",
|
||||
ProjectPath = "samples/03-workflows/Observability/WorkflowAsAnAgent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Interactive console with ReadLine loop; requires OTLP endpoint.",
|
||||
},
|
||||
|
||||
@@ -384,7 +384,7 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_ConfirmInput",
|
||||
ProjectPath = "samples/03-workflows/Declarative/ConfirmInput",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
Inputs = ["hello", "hello"],
|
||||
InputDelayMs = 8000,
|
||||
ExpectedOutputDescription = ["The output should show a confirmation prompt and a user response."],
|
||||
@@ -394,10 +394,10 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_CustomerSupport",
|
||||
ProjectPath = "samples/03-workflows/Declarative/CustomerSupport",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
Inputs = ["My laptop won't start", "The laptop is now working, thank you!"],
|
||||
InputDelayMs = 5000,
|
||||
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."],
|
||||
},
|
||||
|
||||
@@ -405,16 +405,26 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_DeepResearch",
|
||||
ProjectPath = "samples/03-workflows/Declarative/DeepResearch",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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 = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
SkipReason = "Requires a workflow file path as a CLI argument.",
|
||||
},
|
||||
|
||||
@@ -422,8 +432,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_FunctionTools",
|
||||
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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."],
|
||||
@@ -433,7 +443,7 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_HostedWorkflow",
|
||||
ProjectPath = "samples/03-workflows/Declarative/HostedWorkflow",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
SkipReason = "Hosts a persistent workflow server that does not exit.",
|
||||
},
|
||||
|
||||
@@ -441,9 +451,9 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_InputArguments",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InputArguments",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
Inputs = ["I'd like to visit Seattle", "Seattle, WA", "EXIT"],
|
||||
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."],
|
||||
},
|
||||
@@ -452,8 +462,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeFunctionTool",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InvokeFunctionTool",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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."],
|
||||
@@ -463,8 +473,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeFoundryToolboxMcp",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InvokeFoundryToolboxMcp",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL", "FOUNDRY_TOOLBOX_NAME", "FOUNDRY_AGENT_TOOLSET_API_VERSION"],
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME", "FOUNDRY_TOOLBOX_NAME", "FOUNDRY_AGENT_TOOLSET_API_VERSION"],
|
||||
Inputs = ["How do I use Azure OpenAI with my data?"],
|
||||
InputDelayMs = 3000,
|
||||
ExpectedOutputDescription = ["The output should show a workflow using Foundry Toolbox MCP tools to search Microsoft Learn documentation and web search to provide a summary of results."],
|
||||
@@ -474,8 +484,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_InvokeMcpTool",
|
||||
ProjectPath = "samples/03-workflows/Declarative/InvokeMcpTool",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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."],
|
||||
@@ -485,8 +495,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_Marketing",
|
||||
ProjectPath = "samples/03-workflows/Declarative/Marketing",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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."],
|
||||
@@ -496,8 +506,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_StudentTeacher",
|
||||
ProjectPath = "samples/03-workflows/Declarative/StudentTeacher",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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."],
|
||||
@@ -507,8 +517,8 @@ internal static class WorkflowSamples
|
||||
{
|
||||
Name = "Workflow_Declarative_ToolApproval",
|
||||
ProjectPath = "samples/03-workflows/Declarative/ToolApproval",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
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."],
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.301",
|
||||
"version": "10.0.200",
|
||||
"rollForward": "minor",
|
||||
"allowPrerelease": false
|
||||
},
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,14 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.13.0</VersionPrefix>
|
||||
<VersionPrefix>1.11.0</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<DateSuffix>260703</DateSuffix>
|
||||
<DateSuffix>260622</DateSuffix>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.13.0</GitTag>
|
||||
<GitTag>1.11.0</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -22,34 +22,24 @@ var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
var projectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Create a separate IChatClient for the memory component to use for structured extraction.
|
||||
// The memory component calls the model with a ResponseFormat (JSON schema) to extract user info.
|
||||
// Using a dedicated client here avoids mixing side-channel extraction calls with the agent's
|
||||
// conversation history, and avoids the chicken-and-egg problem of needing an IChatClient
|
||||
// before the main agent is constructed.
|
||||
IChatClient extractionClient =
|
||||
new AIProjectClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetProjectOpenAIClient()
|
||||
.GetResponsesClient()
|
||||
.AsIChatClient(model);
|
||||
// Get the underlying IChatClient to use for the memory component.
|
||||
// The memory provider needs direct IChatClient access for structured extraction.
|
||||
IChatClient chatClient = projectClient
|
||||
.AsAIAgent(new ChatClientAgentOptions { ChatOptions = new() { ModelId = model } })
|
||||
.GetService<IChatClient>()
|
||||
?? throw new InvalidOperationException("Could not retrieve IChatClient from AIProjectClient agent.");
|
||||
|
||||
// Create the agent with instructions and the custom memory context provider.
|
||||
// The memory component is attached to all sessions created by the agent. Here each new memory
|
||||
// component will have its own user info object, so each session will have its own memory.
|
||||
// Create the agent and provide a factory to add our custom memory component to
|
||||
// all sessions created by the agent. Here each new memory component will have its own
|
||||
// user info object, so each session will have its own memory.
|
||||
// In real world applications/services, where the user info would be persisted in a database,
|
||||
// and preferably shared between multiple sessions used by the same user, ensure that the
|
||||
// factory reads the user id from the current context and scopes the memory component
|
||||
// and its storage to that user id.
|
||||
AIAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = model,
|
||||
Instructions = "You are a friendly assistant. Always address the user by their name.",
|
||||
},
|
||||
AIContextProviders = [new UserInfoMemory(extractionClient)]
|
||||
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
|
||||
AIContextProviders = [new UserInfoMemory(chatClient)]
|
||||
});
|
||||
|
||||
// Create a new session for the conversation.
|
||||
@@ -128,17 +118,10 @@ namespace SampleApp
|
||||
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
|
||||
if ((userInfo.UserName is null || userInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
|
||||
{
|
||||
// The Foundry Responses API requires the model name in the request body.
|
||||
// Retrieve it from the client's metadata so callers don't need to pass it separately.
|
||||
var modelId = this._chatClient.GetService<ChatClientMetadata>()?.DefaultModelId
|
||||
?? throw new InvalidOperationException(
|
||||
"Could not retrieve DefaultModelId from the extraction IChatClient. " +
|
||||
"Ensure the client was created with a model ID (e.g., via projectClient.AsAIAgent(...)).");
|
||||
var result = await this._chatClient.GetResponseAsync<UserInfo>(
|
||||
context.RequestMessages,
|
||||
new ChatOptions()
|
||||
{
|
||||
ModelId = modelId,
|
||||
Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
@@ -35,7 +35,7 @@ A basic AG-UI server and client that demonstrate the foundational concepts.
|
||||
A basic AG-UI server that hosts an AI agent accessible via HTTP. Demonstrates:
|
||||
|
||||
- Creating an ASP.NET Core web application
|
||||
- Setting up an AG-UI server endpoint with `MapAGUIServer`
|
||||
- Setting up an AG-UI server endpoint with `MapAGUI`
|
||||
- Creating an AI agent from an Azure OpenAI chat client
|
||||
- Streaming responses via Server-Sent Events (SSE)
|
||||
|
||||
@@ -204,7 +204,7 @@ dotnet run
|
||||
### Server-Side
|
||||
|
||||
1. Client sends HTTP POST request with messages
|
||||
2. ASP.NET Core endpoint receives the request via `MapAGUIServer`
|
||||
2. ASP.NET Core endpoint receives the request via `MapAGUI`
|
||||
3. Agent processes messages using Agent Framework
|
||||
4. Responses are streamed back as Server-Sent Events (SSE)
|
||||
|
||||
@@ -214,22 +214,16 @@ dotnet run
|
||||
2. Server responds with SSE stream
|
||||
3. Client parses events into `AgentResponseUpdate` objects
|
||||
4. Updates are displayed based on content type
|
||||
5. The client sends the full message history each turn (the stateless AG-UI client does not rely on a server-assigned `ConversationId`)
|
||||
5. `ConversationId` maintains conversation context
|
||||
|
||||
### Protocol Features
|
||||
|
||||
- **HTTP POST** for requests
|
||||
- **Server-Sent Events (SSE)** for streaming responses
|
||||
- **JSON** for event serialization
|
||||
- **Thread IDs** (read from the `RUN_STARTED` event's raw representation) for conversation context. `AGUIChatClient` is stateless and intentionally does not surface a `ConversationId`.
|
||||
- **Thread IDs** (as `ConversationId`) for conversation context
|
||||
- **Run IDs** (as `ResponseId`) for tracking individual executions
|
||||
|
||||
## Security considerations
|
||||
|
||||
`ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace.
|
||||
|
||||
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Refused
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AGUI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
@@ -15,7 +14,7 @@ using HttpClient httpClient = new()
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
AGUIChatClient chatClient = new(httpClient, serverUrl);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
@@ -50,7 +49,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
string? sessionId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
@@ -59,11 +58,9 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
sessionId = chatUpdate.ConversationId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -87,7 +84,7 @@ try
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
@@ -36,6 +36,6 @@ AIAgent agent = chatClient.AsAIAgent(
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
app.MapAGUI("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AGUI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
@@ -15,7 +14,7 @@ using HttpClient httpClient = new()
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
AGUIChatClient chatClient = new(httpClient, serverUrl);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
@@ -50,7 +49,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
string? sessionId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
@@ -59,11 +58,9 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
sessionId = chatUpdate.ConversationId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -119,7 +116,7 @@ try
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
@@ -93,7 +93,7 @@ ChatClientAgent agent = chatClient.AsAIAgent(
|
||||
tools: tools);
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
app.MapAGUI("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AGUI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
@@ -27,7 +26,7 @@ using HttpClient httpClient = new()
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
AGUIChatClient chatClient = new(httpClient, serverUrl);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
@@ -63,7 +62,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
string? sessionId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
@@ -72,11 +71,9 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
sessionId = chatUpdate.ConversationId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -112,7 +109,7 @@ try
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
@@ -36,6 +36,6 @@ AIAgent agent = chatClient.AsAIAgent(
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
app.MapAGUI("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AGUI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:5100";
|
||||
@@ -13,7 +13,7 @@ using HttpClient httpClient = new()
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
AGUIChatClient chatClient = new(httpClient, serverUrl);
|
||||
|
||||
// Create agent
|
||||
ChatClientAgent baseAgent = chatClient.AsAIAgent(
|
||||
|
||||
@@ -25,7 +25,7 @@ builder.Services.AddHttpLogging(logging =>
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
@@ -73,5 +73,5 @@ ChatClientAgent baseAgent = openAIChatClient.AsAIAgent(
|
||||
// Wrap with ServerFunctionApprovalAgent
|
||||
var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions);
|
||||
|
||||
app.MapAGUIServer("/", agent);
|
||||
app.MapAGUI("/", agent);
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AGUI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using RecipeClient;
|
||||
|
||||
@@ -18,7 +17,7 @@ using HttpClient httpClient = new()
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
AGUIChatClient chatClient = new(httpClient, serverUrl);
|
||||
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "recipe-client",
|
||||
@@ -66,7 +65,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
string? sessionId = null;
|
||||
bool stateReceived = false;
|
||||
|
||||
Console.WriteLine();
|
||||
@@ -78,11 +77,9 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
sessionId = chatUpdate.ConversationId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -116,7 +113,7 @@ try
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
|
||||
Console.ResetColor();
|
||||
|
||||
// Display final state if received
|
||||
|
||||
@@ -12,7 +12,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
// Configure to listen on port 8888
|
||||
builder.WebHost.UseUrls("http://localhost:8888");
|
||||
@@ -61,6 +61,6 @@ AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions);
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
app.MapAGUI("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Server;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
@@ -36,9 +34,10 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Check if the client sent state in the request
|
||||
if (options is not ChatClientAgentRunOptions { ChatOptions: { } chatOptions } chatRunOptions ||
|
||||
!chatOptions.TryGetRunAgentInput(out RunAgentInput? agentInput) ||
|
||||
agentInput.State is not { ValueKind: JsonValueKind.Object } state)
|
||||
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
|
||||
!properties.TryGetValue("ag_ui_state", out object? stateObj) ||
|
||||
stateObj is not JsonElement state ||
|
||||
state.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
// No state management requested, pass through to inner agent
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,10 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="OpenAI" />
|
||||
<PackageReference Include="OpenTelemetry" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
@@ -22,7 +25,7 @@
|
||||
</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" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Azure.Monitor.OpenTelemetry.Exporter;
|
||||
using Microsoft.Agents.AI;
|
||||
@@ -96,8 +96,8 @@ Console.WriteLine("""
|
||||
Type your message and press Enter. Type 'exit' or empty message to quit.
|
||||
""");
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT environment variable is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// Log application startup
|
||||
appLogger.LogInformation("OpenTelemetry Aspire Demo application started");
|
||||
@@ -112,19 +112,20 @@ static async Task<string> GetWeatherAsync([Description("The location to get the
|
||||
// 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.
|
||||
using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
|
||||
.AsBuilder()
|
||||
.UseFunctionInvocation()
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the chat client level
|
||||
.Build();
|
||||
|
||||
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
|
||||
// Create the agent with the instrumented chat client
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
||||
name: "OpenTelemetryDemoAgent",
|
||||
tools: [AIFunctionFactory.Create(GetWeatherAsync)],
|
||||
clientFactory: client => client
|
||||
.AsBuilder()
|
||||
.UseFunctionInvocation()
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the chat client level
|
||||
.Build())
|
||||
var agent = new ChatClientAgent(instrumentedChatClient,
|
||||
name: "OpenTelemetryDemoAgent",
|
||||
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
||||
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
|
||||
.AsBuilder()
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
||||
.Build();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenTelemetry Aspire Demo with Microsoft Foundry
|
||||
# OpenTelemetry Aspire Demo with Azure OpenAI
|
||||
|
||||
This demo showcases the integration of OpenTelemetry with the Microsoft Agent Framework using Microsoft Foundry and the .NET Aspire Dashboard for telemetry visualization.
|
||||
This demo showcases the integration of OpenTelemetry with the Microsoft Agent Framework using Azure OpenAI and .NET Aspire Dashboard for telemetry visualization.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -15,7 +15,7 @@ The demo consists of three main components:
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Console App<br/>(Interactive)"] --> B["Agent Framework<br/>with OpenTel<br/>Instrumentation"]
|
||||
B --> C["Microsoft Foundry<br/>Project"]
|
||||
B --> C["Azure OpenAI<br/>Service"]
|
||||
A --> D["Aspire Dashboard<br/>(OpenTelemetry Visualization)"]
|
||||
B --> D
|
||||
```
|
||||
@@ -23,21 +23,21 @@ graph TD
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry project endpoint and model configured
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- Docker installed (for running Aspire Dashboard)
|
||||
- [Optional] Application Insights and Grafana
|
||||
|
||||
## Configuration
|
||||
|
||||
### Microsoft Foundry Setup
|
||||
### Azure OpenAI Setup
|
||||
Set the following environment variables:
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://<your-project>.services.ai.azure.com/api/projects/<your-project>"
|
||||
$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Foundry project.
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource.
|
||||
|
||||
### [Optional] Application Insights Setup
|
||||
Set the following environment variables:
|
||||
@@ -56,7 +56,7 @@ The easiest way to run the demo is using the provided PowerShell script:
|
||||
```
|
||||
|
||||
This script will automatically:
|
||||
- ✅ Check prerequisites (Docker, Foundry configuration)
|
||||
- ✅ Check prerequisites (Docker, Azure OpenAI configuration)
|
||||
- 🔨 Build the console application
|
||||
- 🐳 Start the Aspire Dashboard via Docker (with anonymous access)
|
||||
- ⏳ Wait for dashboard to be ready (polls port until listening)
|
||||
@@ -124,7 +124,7 @@ You:
|
||||
3. Each trace contains:
|
||||
- An outer span for the entire agent interaction
|
||||
- Inner spans from the Agent Framework's OpenTelemetry instrumentation
|
||||
- Spans from HTTP calls to Microsoft Foundry
|
||||
- Spans from HTTP calls to Azure OpenAI
|
||||
|
||||
### Metrics
|
||||
1. Navigate to the **Metrics** tab
|
||||
@@ -158,7 +158,7 @@ Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
|
||||
- **Telemetry correlation** across the entire request flow
|
||||
|
||||
### Agent Framework Features
|
||||
- **ChatClientAgent** created from `AIProjectClient`
|
||||
- **ChatClientAgent** with Azure OpenAI integration
|
||||
- **OpenTelemetry wrapper** using `.WithOpenTelemetry()`
|
||||
- **Conversation threading** for multi-turn conversations
|
||||
- **Error handling** with telemetry correlation
|
||||
@@ -182,7 +182,7 @@ Complete demo startup script that handles everything automatically.
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- **Automatic configuration detection** - Checks for Foundry configuration
|
||||
- **Automatic configuration detection** - Checks for Azure OpenAI configuration
|
||||
- **Project building** - Automatically builds projects before running
|
||||
- **Error handling** - Provides clear error messages if something goes wrong
|
||||
- **Multi-window support** - Opens dashboard in separate window for better experience
|
||||
@@ -201,10 +201,10 @@ If you encounter port binding errors, try:
|
||||
2. Or kill any processes using the conflicting ports
|
||||
|
||||
### Authentication Issues
|
||||
- Ensure your Foundry project endpoint is correctly configured
|
||||
- Ensure your Azure OpenAI endpoint is correctly configured
|
||||
- Check that the environment variables are set in the correct terminal session
|
||||
- Verify you're logged in with Azure CLI (`az login`) and have access to the Foundry project
|
||||
- Ensure the `FOUNDRY_MODEL` value matches an enabled model in your Foundry project
|
||||
- Verify you're logged in with Azure CLI (`az login`) and have access to the Azure OpenAI resource
|
||||
- Ensure the Azure OpenAI deployment name matches your actual deployment
|
||||
|
||||
### Build Issues
|
||||
- Ensure you're using .NET 10.0 SDK
|
||||
@@ -216,7 +216,7 @@ If you encounter port binding errors, try:
|
||||
```
|
||||
AgentOpenTelemetry/
|
||||
├── AgentOpenTelemetry.csproj # Project file with dependencies
|
||||
├── Program.cs # Main application with Foundry AIProjectClient agent integration
|
||||
├── Program.cs # Main application with Azure OpenAI agent integration
|
||||
├── start-demo.ps1 # PowerShell script to start the demo
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
// 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.
|
||||
// You must dissable client side conversation storage for clients that support it
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ List<AITool> agentTools = [.. mcpTools.Cast<AITool>()];
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
|
||||
instructions: "You are a helpful assistant that can help with Microsoft documentation questions. Use the Microsoft Learn MCP tool to search for documentation. In the output, indicate which tool you used if any.",
|
||||
instructions: "You are a helpful assistant that can help with Microsoft documentation questions. Use the Microsoft Learn MCP tool to search for documentation.",
|
||||
name: "DocsAgent",
|
||||
tools: agentTools);
|
||||
|
||||
|
||||
-31
@@ -8,7 +8,6 @@ using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Assistants;
|
||||
using OpenAI.Responses;
|
||||
|
||||
const string AgentInstructions = "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question.";
|
||||
const string AgentName = "CoderAgent-RAPI";
|
||||
@@ -20,41 +19,11 @@ string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// The easiest way to add the hosted code interpreter is as follows:
|
||||
/*
|
||||
AIAgent agent = aiProjectClient.AsAIAgent(
|
||||
deploymentName,
|
||||
instructions: AgentInstructions,
|
||||
name: AgentName,
|
||||
tools: [new HostedCodeInterpreterTool() { Inputs = [] }]);
|
||||
*/
|
||||
|
||||
// However, by default the reponses API does not return the output items from the hosted code interpreter tool.
|
||||
// This is generally fine but for this sample we want to explicitly request those in the response generation configuration.
|
||||
AIAgent agent = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
.GetProjectResponsesClient()
|
||||
.AsIChatClient(deploymentName)
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(x =>
|
||||
{
|
||||
var previousFactory = x.RawRepresentationFactory;
|
||||
x.RawRepresentationFactory = state =>
|
||||
{
|
||||
var responseOptions = previousFactory?.Invoke(state) as CreateResponseOptions ?? new CreateResponseOptions();
|
||||
|
||||
// Ensure that the response includes tool output items from the hosted code interpreter
|
||||
responseOptions.IncludedProperties.Add(IncludedResponseProperty.CodeInterpreterCallOutputs);
|
||||
|
||||
return responseOptions;
|
||||
};
|
||||
})
|
||||
.Build()
|
||||
.AsAIAgent(
|
||||
instructions: AgentInstructions,
|
||||
name: AgentName,
|
||||
tools: [new HostedCodeInterpreterTool() { Inputs = [] }]);
|
||||
|
||||
AgentResponse response = await agent.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
|
||||
|
||||
|
||||
+28
-31
@@ -12,7 +12,7 @@ using Microsoft.Extensions.AI;
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
const string AgentInstructions = "You are a helpful assistant that can retrieve the latest currency exchange rates using the Frankfurter API. Always call the API to get live data rather than guessing.";
|
||||
const string AgentInstructions = "You are a helpful assistant that can use the countries API to retrieve information about countries by their currency code.";
|
||||
// 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.
|
||||
@@ -25,46 +25,36 @@ AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
|
||||
name: "OpenAPIToolsAgent",
|
||||
tools: [openApiTool]);
|
||||
|
||||
// Run the agent with a question about EUR exchange rates
|
||||
Console.WriteLine(await agent.RunAsync("What is the latest EUR exchange rate against the US Dollar (USD) and British Pound (GBP)?"));
|
||||
// Run the agent with a question about countries
|
||||
Console.WriteLine(await agent.RunAsync("What countries use the Euro (EUR) as their currency? Please list them."));
|
||||
|
||||
OpenApiFunctionDefinition CreateOpenAPIFunctionDefinition()
|
||||
{
|
||||
// OpenAPI spec for Frankfurter — a free, no-auth exchange rate API backed by ECB data.
|
||||
// See https://www.frankfurter.dev/ for documentation.
|
||||
const string FrankfurterOpenApiSpec = """
|
||||
// A simple OpenAPI specification for the REST Countries API
|
||||
const string CountriesOpenApiSpec = """
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Frankfurter Exchange Rate API",
|
||||
"description": "Free currency exchange rates from the European Central Bank",
|
||||
"version": "v1"
|
||||
"title": "REST Countries API",
|
||||
"description": "Retrieve information about countries by currency code",
|
||||
"version": "v3.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.frankfurter.dev/v1"
|
||||
"url": "https://restcountries.com/v3.1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/latest": {
|
||||
"/currency/{currency}": {
|
||||
"get": {
|
||||
"description": "Get the latest exchange rates for a given base currency",
|
||||
"operationId": "GetLatestExchangeRates",
|
||||
"description": "Get countries that use a specific currency code (e.g., USD, EUR, GBP)",
|
||||
"operationId": "GetCountriesByCurrency",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "from",
|
||||
"in": "query",
|
||||
"description": "Base currency code (e.g. EUR, USD, GBP). Defaults to EUR.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"in": "query",
|
||||
"description": "Comma-separated list of target currency codes (e.g. USD,GBP,JPY).",
|
||||
"required": false,
|
||||
"name": "currency",
|
||||
"in": "path",
|
||||
"description": "Currency code (e.g., USD, EUR, GBP)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -72,14 +62,20 @@ OpenApiFunctionDefinition CreateOpenAPIFunctionDefinition()
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Latest exchange rates",
|
||||
"description": "Successful response with list of countries",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No countries found for the currency"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,11 +84,12 @@ OpenApiFunctionDefinition CreateOpenAPIFunctionDefinition()
|
||||
}
|
||||
""";
|
||||
|
||||
// Create the OpenAPI function definition
|
||||
return new(
|
||||
"get_exchange_rates",
|
||||
BinaryData.FromString(FrankfurterOpenApiSpec),
|
||||
"get_countries",
|
||||
BinaryData.FromString(CountriesOpenApiSpec),
|
||||
new OpenAPIAnonymousAuthenticationDetails())
|
||||
{
|
||||
Description = "Get live currency exchange rates from the European Central Bank via Frankfurter"
|
||||
Description = "Retrieve information about countries by currency code"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ This sample shows how to use OpenAPI tools with a `ChatClientAgent` using the Re
|
||||
## What this sample demonstrates
|
||||
|
||||
- Defining an OpenAPI specification inline
|
||||
- Creating an `OpenAPIFunctionDefinition` for the Frankfurter exchange rate API
|
||||
- Creating an `OpenAPIFunctionDefinition` for the REST Countries API
|
||||
- Using `FoundryAITool.CreateOpenApiTool()` with `ChatClientAgent`
|
||||
- Server-side execution of OpenAPI tool calls
|
||||
|
||||
|
||||
+6
-7
@@ -90,7 +90,7 @@ static async Task<string> CreateSampleToolboxAsync(string name, string endpoint,
|
||||
// Delete existing toolbox if present (ignore 404).
|
||||
try
|
||||
{
|
||||
await toolboxClient.DeleteAsync(name);
|
||||
await toolboxClient.DeleteToolboxAsync(name);
|
||||
Console.WriteLine($"Deleted existing toolbox '{name}'");
|
||||
}
|
||||
catch (ClientResultException ex) when (ex.Status == 404)
|
||||
@@ -99,13 +99,12 @@ static async Task<string> CreateSampleToolboxAsync(string name, string endpoint,
|
||||
}
|
||||
|
||||
// Create a fresh version with a single MCP tool.
|
||||
MCPToolboxTool mcpTool = new("api-specs")
|
||||
{
|
||||
ServerUri = new Uri("https://gitmcp.io/Azure/azure-rest-api-specs"),
|
||||
ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval),
|
||||
};
|
||||
ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
|
||||
serverLabel: "api-specs",
|
||||
serverUri: new Uri("https://gitmcp.io/Azure/azure-rest-api-specs"),
|
||||
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
|
||||
|
||||
ToolboxVersion created = (await toolboxClient.CreateVersionAsync(
|
||||
ToolboxVersion created = (await toolboxClient.CreateToolboxVersionAsync(
|
||||
name: name,
|
||||
tools: [mcpTool],
|
||||
description: "Sample toolbox with an MCP tool — created by Agent_Step25 sample.")).Value;
|
||||
|
||||
-37
@@ -64,43 +64,6 @@ AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello wo
|
||||
Console.WriteLine(response);
|
||||
```
|
||||
|
||||
## Approving or denying tool execution
|
||||
|
||||
The GitHub Copilot SDK owns the tool-calling loop for this provider, so approval is enforced through the SDK's
|
||||
native pre-execution hook rather than the Agent Framework chat-client approval round-trip.
|
||||
|
||||
When you register a tool wrapped in `ApprovalRequiredAIFunction`, `GitHubCopilotAgent` installs a default
|
||||
`SessionConfig.Hooks.OnPreToolUse` hook that returns `"ask"` for that tool and defers (`null`) for all other tools.
|
||||
The `"ask"` decision routes to your `SessionConfig.OnPermissionRequest` handler, where you approve or deny the call
|
||||
(this also fires even for tools configured with `SkipPermission = true`):
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot;
|
||||
|
||||
AIFunction deleteFile = AIFunctionFactory.Create(DeleteFile, "DeleteFile", "Deletes a file.");
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
// Wrapping the tool marks it approval-required; the agent turns this into an "ask" at OnPreToolUse.
|
||||
Tools = [new ApprovalRequiredAIFunction(deleteFile)],
|
||||
|
||||
// OnPermissionRequest decides the "asked" tools (and Copilot's built-in shell/file/URL prompts).
|
||||
OnPermissionRequest = (request, invocation) =>
|
||||
{
|
||||
// Surface to a human, check policy, etc.
|
||||
bool approved = AskHuman(request);
|
||||
return Task.FromResult(approved
|
||||
? PermissionDecision.ApproveOnce()
|
||||
: PermissionDecision.Reject("Denied by user."));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
> **⚠️ If you provide your own `OnPreToolUse` hook**, it takes precedence and the agent does **not** install its
|
||||
> default approval hook. In that case **you are fully responsible** for enforcing approval — including for any
|
||||
> `ApprovalRequiredAIFunction` you register (e.g. by returning a `"deny"` or `"ask"` `PreToolUseHookOutput`). The
|
||||
> agent logs a warning when it detects an approval-required tool that your hook must handle.
|
||||
|
||||
## Streaming Responses
|
||||
|
||||
To get streaming responses:
|
||||
|
||||
+8
-4
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -6,16 +6,20 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<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>
|
||||
|
||||
<!-- Copy skills directory to output -->
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
//
|
||||
// This sample uses a unit-converter skill that converts between miles, kilometers, pounds, and kilograms.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
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-5.4-mini";
|
||||
|
||||
// --- Skills Provider ---
|
||||
// Discovers skills from the 'skills' directory containing SKILL.md files.
|
||||
@@ -28,27 +29,18 @@ var skillsProvider = new AgentSkillsProvider(
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
})
|
||||
.AsBuilder()
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
// NOTE: Auto-approving all skill tools is done here for simplicity in
|
||||
// this demonstration. In production, you should prompt the user before
|
||||
// allowing script execution. See Agent_Step07_SkillsAutoApproval for a
|
||||
// walkthrough of the full approval flow.
|
||||
AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
|
||||
})
|
||||
.Build();
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
Console.WriteLine("Converting units with file-based skills");
|
||||
|
||||
+7
-3
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -6,12 +6,16 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
// 3. Code scripts — executable delegates the agent can invoke directly
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
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-5.4-mini";
|
||||
|
||||
// --- Build the code-defined skill ---
|
||||
var unitConverterSkill = new AgentInlineSkill(
|
||||
@@ -69,17 +70,18 @@ var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
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");
|
||||
|
||||
+7
-3
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -6,12 +6,16 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);IDE0051</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
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-5.4-mini";
|
||||
|
||||
// --- Class-Based Skill ---
|
||||
// Instantiate the skill class.
|
||||
@@ -24,17 +25,18 @@ var skillsProvider = new AgentSkillsProvider(unitConverter);
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
});
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
Console.WriteLine("Converting units with class-based skills");
|
||||
|
||||
+8
-4
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -6,16 +6,20 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);IDE0051</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<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>
|
||||
|
||||
<!-- Copy skills directory to output -->
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
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-5.4-mini";
|
||||
|
||||
// --- 1. Code-Defined Skill: volume-converter ---
|
||||
var volumeConverterSkill = new AgentInlineSkill(
|
||||
@@ -66,17 +67,18 @@ var skillsProvider = new AgentSkillsProviderBuilder()
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "MultiConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful assistant that can convert units, volumes, and temperatures.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
});
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Example: Use all three skills ---
|
||||
Console.WriteLine("Converting with mixed skills (file + code + class)");
|
||||
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -6,16 +6,17 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);CA1812;IDE0051</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;CA1812;IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// --- Configuration ---
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
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-5.4-mini";
|
||||
|
||||
// --- DI Container ---
|
||||
// Register application services that skill resources and scripts can resolve at execution time.
|
||||
@@ -82,18 +83,19 @@ var skillsProvider = new AgentSkillsProvider(distanceSkill, weightSkill);
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName,
|
||||
services: serviceProvider);
|
||||
|
||||
// --- Example: Unit conversion ---
|
||||
|
||||
+3
-2
@@ -6,10 +6,11 @@
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MAAI001;MCPEXP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
@@ -17,7 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// to discover and inject the skill into a ChatClientAgent.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -20,6 +20,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenAI.Responses;
|
||||
|
||||
if (args.Length > 0 && args[0] == "--server")
|
||||
{
|
||||
@@ -28,9 +29,9 @@ if (args.Length > 0 && args[0] == "--server")
|
||||
}
|
||||
|
||||
// --- Configuration ---
|
||||
string openAiEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// --- MCP client + skill discovery ---
|
||||
// Launch this same assembly as a stdio MCP server in a child process.
|
||||
@@ -53,27 +54,18 @@ var skillsProvider = new AgentSkillsProviderBuilder()
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(openAiEndpoint), new DefaultAzureCredential())
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(openAiEndpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "SkillsAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful assistant. Use available skills to answer the user.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
})
|
||||
.AsBuilder()
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
// NOTE: Auto-approving all skill tools is done here for simplicity in
|
||||
// this demonstration. In production, you should prompt the user before
|
||||
// allowing skill tools to execute. See Agent_Step07_SkillsAutoApproval
|
||||
// for a walkthrough of the full approval flow.
|
||||
AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
|
||||
})
|
||||
.Build();
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Run ---
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
@@ -32,12 +32,3 @@ $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Discovering skills over MCP means an external MCP server controls what skill content (including
|
||||
instructions and, for archive-type entries, extracted files) reaches the agent. A compromised or
|
||||
untrustworthy server could return adversarial content designed to manipulate the agent (indirect
|
||||
prompt injection) or to exfiltrate data through skill instructions/scripts. Only connect `UseMcpSkills`
|
||||
to MCP servers you have vetted and trust, and keep the conservative archive size/file-count limits in
|
||||
`AgentMcpSkillsSourceOptions` unless you have a specific reason to raise them.
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy skills directory to output -->
|
||||
<ItemGroup>
|
||||
<None Include="skills\**\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to configure auto-approval rules for skill tools using the
|
||||
// UseToolApproval middleware. It builds on the file-based skills pattern from Step01, adding
|
||||
// ToolApprovalAgent middleware with auto-approval rules so that read-only skill operations
|
||||
// (load_skill, read_skill_resource) are approved automatically while script execution
|
||||
// (run_skill_script) still requires explicit user approval.
|
||||
//
|
||||
// All tools exposed by AgentSkillsProvider always require approval by default.
|
||||
// Auto-approval rules let you selectively bypass the approval prompt for safe operations.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.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-5.4-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 ---
|
||||
// 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())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "UnitConverterAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant that can convert units.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName)
|
||||
.AsBuilder()
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
// Auto-approve read-only skill tools (load_skill, read_skill_resource).
|
||||
// run_skill_script will still require explicit user approval.
|
||||
AutoApprovalRules = [AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule],
|
||||
})
|
||||
.Build();
|
||||
|
||||
// For other auto-approval options (all tools, custom lambdas, combining providers),
|
||||
// see the README.md in this sample directory.
|
||||
|
||||
// --- Example: Unit conversion with auto-approval ---
|
||||
Console.WriteLine("Converting units with file-based skills and auto-approval");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?",
|
||||
session);
|
||||
|
||||
// Handle any pending approval requests (only script execution should require approval)
|
||||
List<ToolApprovalRequestContent> approvalRequests = response.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<ToolApprovalRequestContent>()
|
||||
.ToList();
|
||||
|
||||
while (approvalRequests.Count > 0)
|
||||
{
|
||||
List<ChatMessage> userInputResponses = approvalRequests
|
||||
.ConvertAll(functionApprovalRequest =>
|
||||
{
|
||||
var toolCall = (FunctionCallContent)functionApprovalRequest.ToolCall;
|
||||
Console.WriteLine($"Approval required for: {toolCall.Name}. Reply Y to approve:");
|
||||
bool approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
|
||||
});
|
||||
|
||||
response = await agent.RunAsync(userInputResponses, session);
|
||||
approvalRequests = response.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<ToolApprovalRequestContent>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
@@ -1,102 +0,0 @@
|
||||
# Skills Auto-Approval Sample
|
||||
|
||||
This sample demonstrates how to configure **auto-approval rules** for skill tools using the `UseToolApproval` middleware and `AgentSkillsProvider`'s built-in approval rules.
|
||||
|
||||
It builds on the [file-based skills sample](../Agent_Step01_FileBasedSkills/) by adding `ToolApprovalAgent` middleware that auto-approves read-only skill operations while still prompting for script execution.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- All tools exposed by `AgentSkillsProvider` (`load_skill`, `read_skill_resource`, `run_skill_script`) always require approval by default
|
||||
- Multiple ways to configure auto-approval (see below)
|
||||
- Handling approval prompts for script execution via `ToolApprovalRequestContent`
|
||||
|
||||
## Configuring Auto-Approval
|
||||
|
||||
Auto-approval rules are passed to `ToolApprovalAgentOptions.AutoApprovalRules` when calling `UseToolApproval`. Rules are evaluated in order; the first rule returning `true` auto-approves the call.
|
||||
|
||||
### Option 1: Built-in read-only rule
|
||||
|
||||
Auto-approves `load_skill` and `read_skill_resource` while still prompting for `run_skill_script`:
|
||||
|
||||
```csharp
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
AutoApprovalRules = [AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule],
|
||||
})
|
||||
```
|
||||
|
||||
### Option 2: Built-in all-tools rule
|
||||
|
||||
Auto-approves all three skill tools without prompting:
|
||||
|
||||
```csharp
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
|
||||
})
|
||||
```
|
||||
|
||||
### Option 3: Custom lambda rule
|
||||
|
||||
Provide your own logic as a `Func<FunctionCallContent, ValueTask<bool>>`. For example, to auto-approve only `load_skill`:
|
||||
|
||||
```csharp
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
AutoApprovalRules =
|
||||
[
|
||||
(FunctionCallContent functionCall) =>
|
||||
new ValueTask<bool>(functionCall.Name == AgentSkillsProvider.LoadSkillToolName),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Combining rules from multiple providers
|
||||
|
||||
When using multiple providers (e.g., skills + file access), combine their rules in a single list:
|
||||
|
||||
```csharp
|
||||
.UseToolApproval(new ToolApprovalAgentOptions
|
||||
{
|
||||
AutoApprovalRules =
|
||||
[
|
||||
AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule,
|
||||
FileAccessProvider.ReadOnlyToolsAutoApprovalRule,
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## 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-5.4-mini"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
- `load_skill` and `read_skill_resource` calls are auto-approved (no user prompt)
|
||||
- `run_skill_script` calls prompt the user for approval before executing
|
||||
-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()
|
||||
+6
-1
@@ -9,8 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+7
-5
@@ -4,13 +4,14 @@
|
||||
// code interpreter: the model can write and execute arbitrary Python code to
|
||||
// answer quantitative questions without calling any additional tools.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hyperlight;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH") ?? throw new InvalidOperationException("HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
|
||||
|
||||
using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptions.CreateForWasm(guestPath));
|
||||
@@ -18,12 +19,13 @@ using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptio
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant. When the user asks something quantitative, write Python and call `execute_code` instead of guessing." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful assistant. When the user asks something quantitative, write Python and call `execute_code` instead of guessing." },
|
||||
AIContextProviders = [codeAct],
|
||||
});
|
||||
|
||||
|
||||
+6
-1
@@ -9,8 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+7
-5
@@ -7,14 +7,15 @@
|
||||
// ApprovalRequiredAIFunction so any code that reaches it requires user approval
|
||||
// for the entire execute_code invocation.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hyperlight;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH") ?? throw new InvalidOperationException("HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
|
||||
|
||||
AIFunction fetchDocs = AIFunctionFactory.Create(
|
||||
@@ -41,12 +42,13 @@ using var codeAct = new HyperlightCodeActProvider(options);
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant. Prefer orchestrating your work in a single `execute_code` block using `call_tool(...)` over issuing many direct tool calls." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful assistant. Prefer orchestrating your work in a single `execute_code` block using `call_tool(...)` over issuing many direct tool calls." },
|
||||
AIContextProviders = [codeAct],
|
||||
});
|
||||
|
||||
|
||||
+6
-1
@@ -9,8 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+7
-5
@@ -5,14 +5,15 @@
|
||||
// when you want a fixed tool surface for the agent's lifetime and don't need
|
||||
// the per-run snapshot/registry semantics of HyperlightCodeActProvider.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hyperlight;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH") ?? throw new InvalidOperationException("HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
|
||||
|
||||
AIFunction calculate = AIFunctionFactory.Create(
|
||||
@@ -33,9 +34,10 @@ var instructions =
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, instructions: instructions, tools: [executeCode]);
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: instructions, tools: [executeCode]);
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("What is 12.3 * 4.5? Use the multiply tool from within `execute_code`."));
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,12 +9,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
+11
-10
@@ -3,18 +3,17 @@
|
||||
// This sample shows how to create and use a simple AI agent that stores chat messages in a vector store using the ChatHistoryMemoryProvider.
|
||||
// It can then use the chat history from prior conversations to inform responses in new conversations.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.InMemory;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
|
||||
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
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-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
||||
|
||||
// Create a vector store to store the chat messages in.
|
||||
// For demonstration purposes, we are using an in-memory vector store.
|
||||
@@ -24,17 +23,19 @@ VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions
|
||||
// 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.
|
||||
EmbeddingGenerator = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetEmbeddingClient(embeddingDeploymentName)
|
||||
.AsIEmbeddingGenerator()
|
||||
});
|
||||
|
||||
// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in the vector store.
|
||||
AIAgent agent = aiProjectClient
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." },
|
||||
ChatOptions = new() { Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
AIContextProviders = [new ChatHistoryMemoryProvider(
|
||||
vectorStore,
|
||||
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,8 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mem0\Microsoft.Agents.AI.Mem0.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+10
-7
@@ -6,13 +6,15 @@
|
||||
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Mem0;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
var mem0ServiceUri = Environment.GetEnvironmentVariable("MEM0_ENDPOINT") ?? throw new InvalidOperationException("MEM0_ENDPOINT is not set.");
|
||||
var mem0ApiKey = Environment.GetEnvironmentVariable("MEM0_API_KEY") ?? throw new InvalidOperationException("MEM0_API_KEY is not set.");
|
||||
@@ -22,15 +24,16 @@ using HttpClient mem0HttpClient = new();
|
||||
mem0HttpClient.BaseAddress = new Uri(mem0ServiceUri);
|
||||
mem0HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", mem0ApiKey);
|
||||
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// 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 = aiProjectClient
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
|
||||
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
|
||||
// The stateInitializer can be used to customize the Mem0 scope per session and it will be called each time a session
|
||||
// is encountered by the Mem0Provider that does not already have Mem0Provider state stored on the session.
|
||||
// If each session should have its own Mem0 scope, you can create a new id per session via the stateInitializer, e.g.:
|
||||
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,8 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Valkey\Microsoft.Agents.AI.Valkey.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+7
-5
@@ -8,15 +8,16 @@
|
||||
// docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
|
||||
// - Azure OpenAI endpoint and deployment configured via environment variables
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Valkey;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
using Valkey.Glide;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var valkeyConnection = Environment.GetEnvironmentVariable("VALKEY_CONNECTION") ?? "localhost:6379";
|
||||
|
||||
var connection = await ConnectionMultiplexer.ConnectAsync(valkeyConnection);
|
||||
@@ -32,10 +33,11 @@ var historyProvider = new ValkeyChatHistoryProvider(
|
||||
MaxMessages = 20
|
||||
});
|
||||
|
||||
AIAgent historyAgent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
AIAgent historyAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant that remembers our conversation." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful assistant that remembers our conversation." },
|
||||
ChatHistoryProvider = historyProvider
|
||||
});
|
||||
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,12 +9,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
+10
-9
@@ -5,30 +5,30 @@
|
||||
// When the agent is invoked, it searches the vector store for relevant older messages and
|
||||
// prepends them as a "memory" context message before the recent session history.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.InMemory;
|
||||
using OpenAI.Chat;
|
||||
using SampleApp;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
||||
|
||||
// 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.
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
var credential = new DefaultAzureCredential();
|
||||
|
||||
// Create a vector store to store overflow chat messages.
|
||||
// For demonstration purposes, we are using an in-memory vector store.
|
||||
// Replace this with a persistent vector store implementation for production scenarios.
|
||||
VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions()
|
||||
{
|
||||
EmbeddingGenerator = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), credential)
|
||||
.GetEmbeddingClient(embeddingDeploymentName)
|
||||
.AsIEmbeddingGenerator()
|
||||
});
|
||||
@@ -49,10 +49,11 @@ var boundedProvider = new BoundedChatHistoryProvider(
|
||||
searchScope: new() { UserId = "UID1" }));
|
||||
|
||||
// Create the agent with the bounded chat history provider.
|
||||
AIAgent agent = aiProjectClient
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), credential)
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant. Answer questions concisely." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful assistant. Answer questions concisely." },
|
||||
Name = "Assistant",
|
||||
ChatHistoryProvider = boundedProvider,
|
||||
});
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,12 +9,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,29 +5,30 @@
|
||||
// The TextSearchProvider runs a search against the vector store via the TextSearchStore before each model invocation and injects the results into the model context.
|
||||
// The TextSearchStore is a sample store implementation that hardcodes a storage schema and uses the vector store to store and retrieve documents.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Samples;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.InMemory;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
|
||||
|
||||
// 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.
|
||||
AIProjectClient aiProjectClient = new(
|
||||
AzureOpenAIClient azureOpenAIClient = new(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// Create an In-Memory vector store that uses the Azure AI Foundry embedding model to generate embeddings.
|
||||
// Create an In-Memory vector store that uses the Azure OpenAI embedding model to generate embeddings.
|
||||
VectorStore vectorStore = new InMemoryVectorStore(new()
|
||||
{
|
||||
EmbeddingGenerator = aiProjectClient.GetProjectOpenAIClient().GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
|
||||
EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
|
||||
});
|
||||
|
||||
// Create a store that defines a storage schema, and uses the vector store to store and retrieve documents.
|
||||
@@ -59,10 +60,11 @@ TextSearchProviderOptions textSearchOptions = new()
|
||||
};
|
||||
|
||||
// Create the AI agent with the TextSearchProvider as the AI context provider.
|
||||
AIAgent agent = aiProjectClient
|
||||
AIAgent agent = azureOpenAIClient
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
|
||||
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
|
||||
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
|
||||
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
|
||||
|
||||
+3
-1
@@ -9,12 +9,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
+11
-9
@@ -4,32 +4,33 @@
|
||||
// While the sample is using Qdrant, it can easily be replaced with any other vector store that implements the Microsoft.Extensions.VectorData abstractions.
|
||||
// The TextSearchProvider runs a search against the vector store before each model invocation and injects the results into the model context.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel.Connectors.Qdrant;
|
||||
using OpenAI.Chat;
|
||||
using Qdrant.Client;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
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 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.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIProjectClient aiProjectClient = new(
|
||||
AzureOpenAIClient azureOpenAIClient = new(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// Create a Qdrant vector store that uses the Azure AI Foundry embedding model to generate embeddings.
|
||||
// Create a Qdrant vector store that uses the Azure OpenAI embedding model to generate embeddings.
|
||||
QdrantClient client = new("localhost");
|
||||
VectorStore vectorStore = new QdrantVectorStore(client, ownsClient: true, new()
|
||||
{
|
||||
EmbeddingGenerator = aiProjectClient.GetProjectOpenAIClient().GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
|
||||
EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
|
||||
});
|
||||
|
||||
// Create a collection and upsert some text into it.
|
||||
@@ -68,10 +69,11 @@ TextSearchProviderOptions textSearchOptions = new()
|
||||
};
|
||||
|
||||
// Create the AI agent with the TextSearchProvider as the AI context provider.
|
||||
AIAgent agent = aiProjectClient
|
||||
AIAgent agent = azureOpenAIClient
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
|
||||
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
|
||||
// Configure a filter on the InMemoryChatHistoryProvider so that we don't persist the messages produced by the TextSearchProvider in chat history.
|
||||
// The default is to persist all messages except those that came from chat history in the first place.
|
||||
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -9,8 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+7
-5
@@ -6,13 +6,14 @@
|
||||
// The provider invokes the custom search function
|
||||
// before each model invocation and injects the results into the model context.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
TextSearchProviderOptions textSearchOptions = new()
|
||||
{
|
||||
@@ -24,12 +25,13 @@ TextSearchProviderOptions textSearchOptions = new()
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
|
||||
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
|
||||
AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)]
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user