mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
94
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fc518b29 | ||
|
|
f3c3efed43 | ||
|
|
bbccb7c28c | ||
|
|
dbc312a78a | ||
|
|
bb9ed63a34 | ||
|
|
6b94315161 | ||
|
|
bc0e65d716 | ||
|
|
4268080c20 | ||
|
|
fe08574a7c | ||
|
|
f970a699d8 | ||
|
|
f29bae8fbc | ||
|
|
c3901a4ddd | ||
|
|
ba617fc3b5 | ||
|
|
afa7834e2e | ||
|
|
c6951c21f6 | ||
|
|
a982428916 | ||
|
|
90a3e5de47 | ||
|
|
49a6e433a3 | ||
|
|
6086a74302 | ||
|
|
fa8cfb7567 | ||
|
|
6de4c24fdd | ||
|
|
a5f355e04a | ||
|
|
0cf48923cd | ||
|
|
cdc4809b8a | ||
|
|
043208241a | ||
|
|
05ebb966cf | ||
|
|
c83a944e85 | ||
|
|
5d98beddf5 | ||
|
|
e0d0ad16a0 | ||
|
|
f36096ce1a | ||
|
|
03e14ca187 | ||
|
|
b298113d15 | ||
|
|
8091d052d8 | ||
|
|
52a8045bb6 | ||
|
|
78d175a1e2 | ||
|
|
b59a854fcd | ||
|
|
8b0db48d33 | ||
|
|
5affc9c333 | ||
|
|
edcc786651 | ||
|
|
07a1e83492 | ||
|
|
fa2a6af443 | ||
|
|
11c8d89ab2 | ||
|
|
6510d6e3c8 | ||
|
|
dd9a4b6321 | ||
|
|
e8ff541ebf | ||
|
|
d2d5384f28 | ||
|
|
1fccf16f11 | ||
|
|
8ed2159c4b | ||
|
|
b000a2cf51 | ||
|
|
0578f4c910 | ||
|
|
e9a606344a | ||
|
|
d2f79930d5 | ||
|
|
b1e9efee7e | ||
|
|
3ee1bb4f9f | ||
|
|
945647a065 | ||
|
|
401a552735 | ||
|
|
718a1f14fd | ||
|
|
f7c5b8d108 | ||
|
|
e6762ea876 | ||
|
|
08abe9e704 | ||
|
|
a84ad42f6d | ||
|
|
ded17b178c | ||
|
|
55dc3ce734 | ||
|
|
9d8e5ca4f5 | ||
|
|
af787569b3 | ||
|
|
3db2004e49 | ||
|
|
efdabd56dc | ||
|
|
371a869e44 | ||
|
|
e532ced950 | ||
|
|
5d8dd4ea4b | ||
|
|
4c4e1d9b87 | ||
|
|
1d301af7d2 | ||
|
|
8fbda1de22 | ||
|
|
ef86fb51d5 | ||
|
|
d5c07f2623 | ||
|
|
ae989b92e7 | ||
|
|
3242d8a4c4 | ||
|
|
e1e6e3d35e | ||
|
|
08697f8037 | ||
|
|
b0f5fa541c | ||
|
|
e3290a2d22 | ||
|
|
200488cb08 | ||
|
|
bd4fc64b4d | ||
|
|
b2e77067e9 | ||
|
|
08541ee5a9 | ||
|
|
dc4bafbc1e | ||
|
|
de6d0267f2 | ||
|
|
0099a6e2fa | ||
|
|
793403f3db | ||
|
|
9fdd7429a8 | ||
|
|
abc9b60ec9 | ||
|
|
6bc0dc5911 | ||
|
|
cf91819625 | ||
|
|
578416a379 |
@@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "Python 3",
|
"name": "Python 3",
|
||||||
"image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
|
"image": "mcr.microsoft.com/devcontainers/python:3.14-bookworm",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
|
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
|
||||||
|
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
|
||||||
|
"ghcr.io/devcontainers/features/copilot-cli:1": {}
|
||||||
},
|
},
|
||||||
"postCreateCommand": "bash ./devsetup.sh",
|
"postCreateCommand": "bash ./devsetup.sh",
|
||||||
"workspaceFolder": "/workspaces/agent-framework/python/",
|
"workspaceFolder": "/workspaces/agent-framework/python/",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
"GitHub.copilot",
|
||||||
|
"GitHub.vscode-github-actions",
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-windows-ai-studio.windows-ai-studio",
|
"ms-windows-ai-studio.windows-ai-studio",
|
||||||
"littlefoxteam.vscode-python-test-adapter"
|
"littlefoxteam.vscode-python-test-adapter"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ignorePatterns:
|
|||||||
- pattern: "./blob"
|
- pattern: "./blob"
|
||||||
- pattern: "./issues"
|
- pattern: "./issues"
|
||||||
- pattern: "./discussions"
|
- pattern: "./discussions"
|
||||||
- pattern: "./pulls"
|
- pattern: "./pull"
|
||||||
- pattern: "https:\/\/platform.openai.com"
|
- pattern: "https:\/\/platform.openai.com"
|
||||||
- pattern: "http:\/\/localhost"
|
- pattern: "http:\/\/localhost"
|
||||||
- pattern: "http:\/\/127.0.0.1"
|
- pattern: "http:\/\/127.0.0.1"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Free runner disk space
|
||||||
|
description: |
|
||||||
|
Reclaims disk space on GitHub-hosted Ubuntu runners by removing
|
||||||
|
pre-installed toolchains we do not use (Android SDK, GHC/Haskell,
|
||||||
|
CodeQL bundle), Docker images, and swap. Also relocates the
|
||||||
|
NuGet package cache to /mnt (which has ~75 GB free vs ~14 GB
|
||||||
|
on /). No-op on non-Linux runners.
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Free disk space (Linux only)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "::group::Disk usage before cleanup"
|
||||||
|
df -h /
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
# Remove pre-installed toolchains we never use on this repo's
|
||||||
|
# dotnet/python jobs. These reclaim ~25-30 GB on ubuntu-latest.
|
||||||
|
sudo rm -rf \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/share/dotnet/sdk/NuGetFallbackFolder \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/opt/hostedtoolcache/CodeQL \
|
||||||
|
/opt/hostedtoolcache/PyPy \
|
||||||
|
/opt/hostedtoolcache/Ruby \
|
||||||
|
/opt/hostedtoolcache/go \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/vcpkg \
|
||||||
|
/usr/local/lib/heroku \
|
||||||
|
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/PyPy" \
|
||||||
|
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/Ruby" \
|
||||||
|
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/go" || true
|
||||||
|
|
||||||
|
# Drop docker images shipped on the runner; jobs that need
|
||||||
|
# docker pull what they need fresh.
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
sudo docker image prune --all --force >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable swap to free its backing file.
|
||||||
|
sudo swapoff -a || true
|
||||||
|
sudo rm -f /mnt/swapfile /swapfile || true
|
||||||
|
|
||||||
|
echo "::group::Disk usage after cleanup"
|
||||||
|
df -h /
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Relocate NuGet package cache to /mnt (Linux only)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo mkdir -p /mnt/nuget
|
||||||
|
sudo chown -R "$USER":"$USER" /mnt/nuget
|
||||||
|
echo "NUGET_PACKAGES=/mnt/nuget" >> "$GITHUB_ENV"
|
||||||
|
echo "Relocated NuGet package cache to /mnt/nuget"
|
||||||
|
df -h /mnt || true
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
function getPullRequest(context) {
|
||||||
|
const pullRequest = context.payload.pull_request;
|
||||||
|
if (!pullRequest?.number || !pullRequest.user?.login) {
|
||||||
|
throw new Error('This script must be run from a pull_request_target event.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
author: pullRequest.user.login,
|
||||||
|
authorType: pullRequest.user.type,
|
||||||
|
labels: pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [],
|
||||||
|
number: pullRequest.number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureLabel({ github, owner, repo, labelName }) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
name: labelName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
name: labelName,
|
||||||
|
color: 'd93f0b',
|
||||||
|
description: 'Community author has exceeded the open pull request limit.',
|
||||||
|
});
|
||||||
|
} catch (createError) {
|
||||||
|
if (createError.status !== 422) {
|
||||||
|
throw createError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasLabel(labels, labelName) {
|
||||||
|
if (!labelName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels.some((label) => label.toLowerCase() === labelName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDependabotAuthor({ author, authorType }) {
|
||||||
|
return authorType === 'Bot' && author.toLowerCase() === 'dependabot[bot]';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) {
|
||||||
|
return [
|
||||||
|
`Thank you for your contribution, @${author}.`,
|
||||||
|
'',
|
||||||
|
`To keep the review queue manageable, we currently limit community contributors to ${maxOpenPrs} `
|
||||||
|
+ `open pull requests at a time. This PR would put you at ${openPrCount} open pull requests, `
|
||||||
|
+ 'so we are closing it automatically.',
|
||||||
|
'',
|
||||||
|
'Please focus on getting your existing PRs reviewed, merged, or closed before opening another one. '
|
||||||
|
+ `If a maintainer asked you to open this PR, they can apply the \`${exemptLabelName}\` label and reopen it.`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber }) {
|
||||||
|
const openPullRequests = await github.paginate(github.rest.pulls.list, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
state: 'open',
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorOpenPullRequestNumbers = openPullRequests
|
||||||
|
.filter((pullRequest) => pullRequest.user?.login === author)
|
||||||
|
.map((pullRequest) => pullRequest.number);
|
||||||
|
const currentPrIsOpen = authorOpenPullRequestNumbers.includes(pullRequestNumber);
|
||||||
|
const existingOpenPrCount = currentPrIsOpen
|
||||||
|
? authorOpenPullRequestNumbers.length - 1
|
||||||
|
: authorOpenPullRequestNumbers.length;
|
||||||
|
|
||||||
|
return existingOpenPrCount + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) {
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const { author, authorType, labels, number } = getPullRequest(context);
|
||||||
|
|
||||||
|
if (isDependabotAuthor({ author, authorType })) {
|
||||||
|
core.info(`Author ${author} is Dependabot; skipping open PR limit enforcement.`);
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
closed: false,
|
||||||
|
dependabotExempt: true,
|
||||||
|
openPrCount: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLabel(labels, exemptLabelName)) {
|
||||||
|
core.info(`PR #${number} has the ${exemptLabelName} label; skipping open PR limit enforcement.`);
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
closed: false,
|
||||||
|
exempt: true,
|
||||||
|
openPrCount: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPrCount = await getOpenPrCount({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
author,
|
||||||
|
pullRequestNumber: number,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (openPrCount <= maxOpenPrs) {
|
||||||
|
core.info(
|
||||||
|
`${author} has ${openPrCount} open pull request(s), which is within the limit of ${maxOpenPrs}.`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
closed: false,
|
||||||
|
openPrCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureLabel({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
labelName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: number,
|
||||||
|
labels: [labelName],
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: number,
|
||||||
|
body: buildLimitMessage({
|
||||||
|
author,
|
||||||
|
exemptLabelName,
|
||||||
|
maxOpenPrs,
|
||||||
|
openPrCount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: number,
|
||||||
|
state: 'closed',
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`${author} has ${openPrCount} open pull request(s), which exceeds the limit of ${maxOpenPrs}. `
|
||||||
|
+ `Closed PR #${number}.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
closed: true,
|
||||||
|
openPrCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildLimitMessage,
|
||||||
|
enforcePrLimit,
|
||||||
|
getOpenPrCount,
|
||||||
|
};
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for pr_limit_moderation.js.
|
||||||
|
*
|
||||||
|
* Run with: node --test .github/tests/test_pr_limit_moderation.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { describe, it } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const { enforcePrLimit } = require('../scripts/pr_limit_moderation.js');
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function createContext({ author = 'community-user', authorType = 'User', labels = [], number = 123 } = {}) {
|
||||||
|
return {
|
||||||
|
repo: {
|
||||||
|
owner: 'microsoft',
|
||||||
|
repo: 'agent-framework',
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
pull_request: {
|
||||||
|
number,
|
||||||
|
labels: labels.map((name) => ({ name })),
|
||||||
|
user: {
|
||||||
|
login: author,
|
||||||
|
type: authorType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCore() {
|
||||||
|
const messages = [];
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
info(message) {
|
||||||
|
messages.push(message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGithub({
|
||||||
|
itemNumbers,
|
||||||
|
labelExists = true,
|
||||||
|
pullRequests = createPullRequestPage({ numbers: itemNumbers }),
|
||||||
|
}) {
|
||||||
|
const calls = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
calls,
|
||||||
|
async paginate(method, params) {
|
||||||
|
calls.push({ api: 'paginate', method, params });
|
||||||
|
return pullRequests;
|
||||||
|
},
|
||||||
|
rest: {
|
||||||
|
issues: {
|
||||||
|
async getLabel(params) {
|
||||||
|
calls.push({ api: 'issues.getLabel', params });
|
||||||
|
if (!labelExists) {
|
||||||
|
const error = new Error('Not Found');
|
||||||
|
error.status = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return { data: { name: params.name } };
|
||||||
|
},
|
||||||
|
async createLabel(params) {
|
||||||
|
calls.push({ api: 'issues.createLabel', params });
|
||||||
|
return { data: { name: params.name } };
|
||||||
|
},
|
||||||
|
async addLabels(params) {
|
||||||
|
calls.push({ api: 'issues.addLabels', params });
|
||||||
|
return { data: [] };
|
||||||
|
},
|
||||||
|
async createComment(params) {
|
||||||
|
calls.push({ api: 'issues.createComment', params });
|
||||||
|
return { data: { id: 1 } };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pulls: {
|
||||||
|
async list(params) {
|
||||||
|
calls.push({ api: 'pulls.list', params });
|
||||||
|
return { data: pullRequests };
|
||||||
|
},
|
||||||
|
async update(params) {
|
||||||
|
calls.push({ api: 'pulls.update', params });
|
||||||
|
return { data: { state: params.state } };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPullRequestPage({ author = 'community-user', numbers }) {
|
||||||
|
return numbers.map((number) => ({
|
||||||
|
number,
|
||||||
|
user: {
|
||||||
|
login: author,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PR limit enforcement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('PR limit enforcement', () => {
|
||||||
|
it('does not close the PR when the author is at the open PR limit', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 123],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext(),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, false);
|
||||||
|
assert.equal(result.openPrCount, 10);
|
||||||
|
assert.deepEqual(
|
||||||
|
github.calls.map((call) => call.api),
|
||||||
|
['paginate'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts the new PR when the pull list includes it', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [123, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext(),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, true);
|
||||||
|
assert.equal(result.openPrCount, 11);
|
||||||
|
assert.deepEqual(
|
||||||
|
github.calls.map((call) => call.api),
|
||||||
|
[
|
||||||
|
'paginate',
|
||||||
|
'issues.getLabel',
|
||||||
|
'issues.addLabels',
|
||||||
|
'issues.createComment',
|
||||||
|
'pulls.update',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts the current PR on top of existing open PRs', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [123, ...Array.from({ length: 24 }, (_, index) => index + 1)],
|
||||||
|
pullRequests: createPullRequestPage({
|
||||||
|
numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext(),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, true);
|
||||||
|
assert.equal(result.openPrCount, 26);
|
||||||
|
const comment = github.calls.find((call) => call.api === 'issues.createComment').params.body;
|
||||||
|
assert.match(comment, /This PR would put you at 26 open pull requests/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates the label when it does not already exist', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||||
|
labelExists: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext(),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, true);
|
||||||
|
assert.deepEqual(
|
||||||
|
github.calls.map((call) => call.api),
|
||||||
|
[
|
||||||
|
'paginate',
|
||||||
|
'issues.getLabel',
|
||||||
|
'issues.createLabel',
|
||||||
|
'issues.addLabels',
|
||||||
|
'issues.createComment',
|
||||||
|
'pulls.update',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
github.calls.find((call) => call.api === 'issues.createLabel').params.name,
|
||||||
|
'too-many-prs',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tolerates a 422 race when creating the label', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||||
|
labelExists: false,
|
||||||
|
});
|
||||||
|
github.rest.issues.createLabel = async (params) => {
|
||||||
|
github.calls.push({ api: 'issues.createLabel', params });
|
||||||
|
const error = new Error('Validation Failed');
|
||||||
|
error.status = 422;
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext(),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, true);
|
||||||
|
assert.deepEqual(
|
||||||
|
github.calls.map((call) => call.api),
|
||||||
|
[
|
||||||
|
'paginate',
|
||||||
|
'issues.getLabel',
|
||||||
|
'issues.createLabel',
|
||||||
|
'issues.addLabels',
|
||||||
|
'issues.createComment',
|
||||||
|
'pulls.update',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses a diplomatic close message with the configured limit', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||||
|
pullRequests: createPullRequestPage({
|
||||||
|
author: 'octo-contributor',
|
||||||
|
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext({ author: 'octo-contributor' }),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
const comment = github.calls.find((call) => call.api === 'issues.createComment').params.body;
|
||||||
|
assert.match(comment, /Thank you for your contribution/);
|
||||||
|
assert.match(comment, /limit community contributors to 10 open pull requests/);
|
||||||
|
assert.match(comment, /@octo-contributor/);
|
||||||
|
assert.match(comment, /`pr-limit-exempt` label and reopen/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not close an exempt PR when it is reopened', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext({ labels: ['PR-LIMIT-EXEMPT'] }),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, false);
|
||||||
|
assert.equal(result.exempt, true);
|
||||||
|
assert.equal(result.openPrCount, null);
|
||||||
|
assert.deepEqual(github.calls, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not close Dependabot PRs', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)],
|
||||||
|
pullRequests: createPullRequestPage({
|
||||||
|
author: 'dependabot[bot]',
|
||||||
|
numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext({ author: 'dependabot[bot]', authorType: 'Bot' }),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, false);
|
||||||
|
assert.equal(result.dependabotExempt, true);
|
||||||
|
assert.equal(result.openPrCount, null);
|
||||||
|
assert.deepEqual(github.calls, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts the current PR when the author has more than one page of open PRs', async () => {
|
||||||
|
const github = createGithub({
|
||||||
|
itemNumbers: [123, ...Array.from({ length: 100 }, (_, index) => index + 1)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context: createContext({ number: 123 }),
|
||||||
|
core: createCore(),
|
||||||
|
exemptLabelName: 'pr-limit-exempt',
|
||||||
|
maxOpenPrs: 10,
|
||||||
|
labelName: 'too-many-prs',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.closed, true);
|
||||||
|
assert.equal(result.openPrCount, 101);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -121,6 +121,9 @@ jobs:
|
|||||||
python
|
python
|
||||||
declarative-agents
|
declarative-agents
|
||||||
|
|
||||||
|
- name: Free runner disk space
|
||||||
|
uses: ./.github/actions/free-runner-disk-space
|
||||||
|
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
@@ -191,6 +194,9 @@ jobs:
|
|||||||
python
|
python
|
||||||
declarative-agents
|
declarative-agents
|
||||||
|
|
||||||
|
- name: Free runner disk space
|
||||||
|
uses: ./.github/actions/free-runner-disk-space
|
||||||
|
|
||||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||||
- name: Start Azure Cosmos DB Emulator
|
- name: Start Azure Cosmos DB Emulator
|
||||||
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
||||||
@@ -365,6 +371,9 @@ jobs:
|
|||||||
dotnet
|
dotnet
|
||||||
python
|
python
|
||||||
|
|
||||||
|
- name: Free runner disk space
|
||||||
|
uses: ./.github/actions/free-runner-disk-space
|
||||||
|
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
@@ -452,6 +461,9 @@ jobs:
|
|||||||
python
|
python
|
||||||
declarative-agents
|
declarative-agents
|
||||||
|
|
||||||
|
- name: Free runner disk space
|
||||||
|
uses: ./.github/actions/free-runner-disk-space
|
||||||
|
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
name: Limit community pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pr-limit-${{ github.repository }}-${{ github.event.pull_request.user.login }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
MAX_OPEN_PULL_REQUESTS: '10'
|
||||||
|
PR_LIMIT_EXEMPT_LABEL: pr-limit-exempt
|
||||||
|
TOO_MANY_PRS_LABEL: too-many-prs
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
team_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
is_team_member: ${{ steps.check.outputs.is_team_member }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout scripts
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Check PR author team membership
|
||||||
|
id: check
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
env:
|
||||||
|
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||||
|
script: |
|
||||||
|
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
|
||||||
|
const { author, isTeamMember } = await checkTeamMembership({
|
||||||
|
github,
|
||||||
|
context,
|
||||||
|
core,
|
||||||
|
teamSlug: process.env.TEAM_NAME,
|
||||||
|
issueNumber: process.env.PR_NUMBER,
|
||||||
|
});
|
||||||
|
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
|
||||||
|
if (isTeamMember) {
|
||||||
|
core.info(`Author ${author} is a team member; skipping open PR limit.`);
|
||||||
|
} else {
|
||||||
|
core.info(`Author ${author} is not a team member; checking open PR limit.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
limit_open_prs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: team_check
|
||||||
|
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout scripts
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Enforce open PR limit
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||||
|
script: |
|
||||||
|
const { enforcePrLimit } = require('./.github/scripts/pr_limit_moderation.js');
|
||||||
|
await enforcePrLimit({
|
||||||
|
github,
|
||||||
|
context,
|
||||||
|
core,
|
||||||
|
exemptLabelName: process.env.PR_LIMIT_EXEMPT_LABEL,
|
||||||
|
maxOpenPrs: Number.parseInt(process.env.MAX_OPEN_PULL_REQUESTS, 10),
|
||||||
|
labelName: process.env.TOO_MANY_PRS_LABEL,
|
||||||
|
});
|
||||||
@@ -23,6 +23,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install Chrome for Puppeteer
|
||||||
|
run: npx puppeteer browsers install chrome
|
||||||
|
|
||||||
# Checks the status of hyperlinks in all files
|
# Checks the status of hyperlinks in all files
|
||||||
- name: Run linkspector
|
- name: Run linkspector
|
||||||
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1
|
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1
|
||||||
|
|||||||
@@ -474,6 +474,45 @@ jobs:
|
|||||||
path: ./python/pytest.xml
|
path: ./python/pytest.xml
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
# GitHub Copilot integration tests
|
||||||
|
python-tests-github-copilot:
|
||||||
|
name: Python Integration Tests - GitHub Copilot
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: integration
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||||
|
GITHUB_COPILOT_TIMEOUT: "120"
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: python
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.checkout-ref }}
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Set up python and install the project
|
||||||
|
id: python-setup
|
||||||
|
uses: ./.github/actions/python-setup
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.UV_PYTHON }}
|
||||||
|
os: ${{ runner.os }}
|
||||||
|
- name: Test with pytest (GitHub Copilot integration)
|
||||||
|
run: >
|
||||||
|
uv run pytest --import-mode=importlib
|
||||||
|
packages/github_copilot/tests
|
||||||
|
-m integration
|
||||||
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
|
--retries 2 --retry-delay 5
|
||||||
|
--junitxml=pytest.xml
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
|
with:
|
||||||
|
name: test-results-github-copilot
|
||||||
|
path: ./python/pytest.xml
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
# Integration test trend report (aggregates per-job JUnit XML results)
|
# Integration test trend report (aggregates per-job JUnit XML results)
|
||||||
python-integration-test-report:
|
python-integration-test-report:
|
||||||
name: Integration Test Report
|
name: Integration Test Report
|
||||||
@@ -490,6 +529,7 @@ jobs:
|
|||||||
python-tests-foundry,
|
python-tests-foundry,
|
||||||
python-tests-foundry-hosting,
|
python-tests-foundry-hosting,
|
||||||
python-tests-cosmos,
|
python-tests-cosmos,
|
||||||
|
python-tests-github-copilot,
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
@@ -553,7 +593,8 @@ jobs:
|
|||||||
python-tests-functions,
|
python-tests-functions,
|
||||||
python-tests-foundry,
|
python-tests-foundry,
|
||||||
python-tests-foundry-hosting,
|
python-tests-foundry-hosting,
|
||||||
python-tests-cosmos
|
python-tests-cosmos,
|
||||||
|
python-tests-github-copilot
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
- name: Fail workflow if tests failed
|
- name: Fail workflow if tests failed
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ jobs:
|
|||||||
foundryChanged: ${{ steps.filter.outputs.foundry }}
|
foundryChanged: ${{ steps.filter.outputs.foundry }}
|
||||||
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
|
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
|
||||||
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
|
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
|
||||||
|
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||||
@@ -85,6 +86,8 @@ jobs:
|
|||||||
- 'python/packages/foundry_hosting/**'
|
- 'python/packages/foundry_hosting/**'
|
||||||
cosmos:
|
cosmos:
|
||||||
- 'python/packages/azure-cosmos/**'
|
- 'python/packages/azure-cosmos/**'
|
||||||
|
github_copilot:
|
||||||
|
- 'python/packages/github_copilot/**'
|
||||||
# run only if 'python' files were changed
|
# run only if 'python' files were changed
|
||||||
- name: python tests
|
- name: python tests
|
||||||
if: steps.filter.outputs.python == 'true'
|
if: steps.filter.outputs.python == 'true'
|
||||||
@@ -658,6 +661,58 @@ jobs:
|
|||||||
path: ./python/pytest.xml
|
path: ./python/pytest.xml
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
# GitHub Copilot integration tests
|
||||||
|
python-tests-github-copilot:
|
||||||
|
name: Python Tests - GitHub Copilot Integration
|
||||||
|
needs: paths-filter
|
||||||
|
if: >
|
||||||
|
github.event_name != 'pull_request' &&
|
||||||
|
needs.paths-filter.outputs.pythonChanges == 'true' &&
|
||||||
|
(github.event_name != 'merge_group' ||
|
||||||
|
needs.paths-filter.outputs.githubCopilotChanged == 'true' ||
|
||||||
|
needs.paths-filter.outputs.coreChanged == 'true')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: integration
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||||
|
GITHUB_COPILOT_TIMEOUT: "120"
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: python
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
- name: Set up python and install the project
|
||||||
|
id: python-setup
|
||||||
|
uses: ./.github/actions/python-setup
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.UV_PYTHON }}
|
||||||
|
os: ${{ runner.os }}
|
||||||
|
- name: Test with pytest (GitHub Copilot integration)
|
||||||
|
run: >
|
||||||
|
uv run pytest --import-mode=importlib
|
||||||
|
packages/github_copilot/tests
|
||||||
|
-m integration
|
||||||
|
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||||
|
--retries 2 --retry-delay 5
|
||||||
|
--junitxml=pytest.xml
|
||||||
|
- name: Surface failing tests
|
||||||
|
if: always()
|
||||||
|
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||||
|
with:
|
||||||
|
path: ./python/pytest.xml
|
||||||
|
summary: true
|
||||||
|
display-options: fEX
|
||||||
|
fail-on-empty: false
|
||||||
|
title: GitHub Copilot integration test results
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
|
with:
|
||||||
|
name: test-results-github-copilot
|
||||||
|
path: ./python/pytest.xml
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
# Integration test trend report (aggregates per-job JUnit XML results)
|
# Integration test trend report (aggregates per-job JUnit XML results)
|
||||||
python-integration-test-report:
|
python-integration-test-report:
|
||||||
name: Integration Test Report
|
name: Integration Test Report
|
||||||
@@ -674,6 +729,7 @@ jobs:
|
|||||||
python-tests-foundry,
|
python-tests-foundry,
|
||||||
python-tests-foundry-hosting,
|
python-tests-foundry-hosting,
|
||||||
python-tests-cosmos,
|
python-tests-cosmos,
|
||||||
|
python-tests-github-copilot,
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
@@ -735,6 +791,7 @@ jobs:
|
|||||||
python-tests-foundry,
|
python-tests-foundry,
|
||||||
python-tests-foundry-hosting,
|
python-tests-foundry-hosting,
|
||||||
python-tests-cosmos,
|
python-tests-cosmos,
|
||||||
|
python-tests-github-copilot,
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
- name: Fail workflow if tests failed
|
- name: Fail workflow if tests failed
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
actions: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -23,7 +24,7 @@ jobs:
|
|||||||
- name: Download coverage report
|
- name: Download coverage report
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
github-token: ${{ github.token }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
path: ./python
|
path: ./python
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -38,9 +39,9 @@ jobs:
|
|||||||
echo "PR number file 'pr_number' is missing or empty"
|
echo "PR number file 'pr_number' is missing or empty"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
PR_NUMBER=$(head -1 pr_number | tr -dc '0-9')
|
PR_NUMBER=$(cat pr_number)
|
||||||
if [ -z "$PR_NUMBER" ]; then
|
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||||
echo "PR number file 'pr_number' does not contain a valid PR number"
|
echo "::error::PR number file contains invalid content"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
|
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
|
||||||
@@ -48,7 +49,7 @@ jobs:
|
|||||||
id: coverageComment
|
id: coverageComment
|
||||||
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
|
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
github-token: ${{ github.token }}
|
||||||
issue-number: ${{ env.PR_NUMBER }}
|
issue-number: ${{ env.PR_NUMBER }}
|
||||||
pytest-xml-coverage-path: python/python-coverage.xml
|
pytest-xml-coverage-path: python/python-coverage.xml
|
||||||
title: "Python Test Coverage Report"
|
title: "Python Test Coverage Report"
|
||||||
|
|||||||
@@ -248,3 +248,4 @@ dotnet/filtered-*.slnx
|
|||||||
.omx/
|
.omx/
|
||||||
|
|
||||||
**/issues/
|
**/issues/
|
||||||
|
.test_*
|
||||||
|
|||||||
+4
-4
@@ -173,11 +173,11 @@ new SampleDefinition
|
|||||||
```csharp
|
```csharp
|
||||||
new SampleDefinition
|
new SampleDefinition
|
||||||
{
|
{
|
||||||
Name = "Workflow_Declarative_GenerateCode",
|
Name = "Workflow_Visualization",
|
||||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
ProjectPath = "samples/03-workflows/Visualization",
|
||||||
IsDeterministic = true,
|
IsDeterministic = true,
|
||||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
MustContain = ["Generating workflow visualization...", "Mermaid string:", "DiGraph string:"],
|
||||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
ExpectedOutputDescription = ["The output should show workflow visualization in Mermaid and DiGraph formats."],
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,14 @@
|
|||||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||||
<!-- Azure.* -->
|
<!-- Azure.* -->
|
||||||
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.23" />
|
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.25" />
|
||||||
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.3" />
|
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.4" />
|
||||||
<PackageVersion Include="Azure.AI.AgentServer.Responses" 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.Search.Documents" Version="12.0.0" />
|
||||||
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.2" />
|
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.2" />
|
||||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
||||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||||
<PackageVersion Include="Azure.Core" Version="1.55.0" />
|
<PackageVersion Include="Azure.Core" Version="1.56.0" />
|
||||||
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
|
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
|
||||||
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
|
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
|
||||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
|
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
|
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
|
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
|
||||||
<PackageVersion Include="System.ClientModel" Version="1.11.0" />
|
<PackageVersion Include="System.ClientModel" Version="1.12.0" />
|
||||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
|
||||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
|
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
|
||||||
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
|
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
|
||||||
<!-- MCP -->
|
<!-- MCP -->
|
||||||
<PackageVersion Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageVersion Include="ModelContextProtocol" Version="1.2.0" />
|
||||||
<!-- Hyperlight -->
|
<!-- Hyperlight -->
|
||||||
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
|
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
|
||||||
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
|
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
|
||||||
|
|||||||
@@ -117,17 +117,18 @@
|
|||||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
|
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
|
<Project Path="samples/02-agents/AgentSkills/Agent_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_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
|
||||||
|
<Project Path="samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills/Agent_Step06_McpBasedSkills.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/Harness/">
|
<Folder Name="/Samples/02-agents/Harness/">
|
||||||
<File Path="samples/02-agents/Harness/README.md" />
|
<File Path="samples/02-agents/Harness/README.md" />
|
||||||
|
<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" />
|
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
|
||||||
<Project Path="samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj" />
|
<Project Path="samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj" />
|
||||||
<Project Path="samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj" />
|
<Project Path="samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj" />
|
||||||
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj" />
|
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj" />
|
||||||
<Project Path="samples/02-agents/Harness/Harness_Step03_DataProcessing/Harness_Step03_DataProcessing.csproj" />
|
<Project Path="samples/02-agents/Harness/Harness_Step03_DataProcessing/Harness_Step03_DataProcessing.csproj" />
|
||||||
<Project Path="samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj" />
|
<Project Path="samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj" />
|
||||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
|
|
||||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||||
@@ -173,6 +174,7 @@
|
|||||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
|
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
|
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Agent_Step25_FoundryToolboxMcp.csproj" />
|
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Agent_Step25_FoundryToolboxMcp.csproj" />
|
||||||
|
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/Evaluation/">
|
<Folder Name="/Samples/02-agents/Evaluation/">
|
||||||
<Project Path="samples/02-agents/Evaluation/Evaluation_CustomEvals/Evaluation_CustomEvals.csproj" />
|
<Project Path="samples/02-agents/Evaluation/Evaluation_CustomEvals/Evaluation_CustomEvals.csproj" />
|
||||||
@@ -212,6 +214,7 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
||||||
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
||||||
|
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj" />
|
||||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
|
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
|
||||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
|
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
|
||||||
<Project Path="samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
|
<Project Path="samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
|
||||||
@@ -240,11 +243,10 @@
|
|||||||
<Project Path="samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.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/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
|
||||||
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.csproj" />
|
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
|
|
||||||
<Project Path="samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj" />
|
<Project Path="samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj" />
|
||||||
|
<Project Path="samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/InvokeHttpRequest/InvokeHttpRequest.csproj" />
|
<Project Path="samples/03-workflows/Declarative/InvokeHttpRequest/InvokeHttpRequest.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
||||||
@@ -281,6 +283,7 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/03-workflows/Orchestration/">
|
<Folder Name="/Samples/03-workflows/Orchestration/">
|
||||||
<Project Path="samples/03-workflows/Orchestration/Handoff/Handoff.csproj" />
|
<Project Path="samples/03-workflows/Orchestration/Handoff/Handoff.csproj" />
|
||||||
|
<Project Path="samples/03-workflows/Orchestration/Magentic/Magentic.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/03-workflows/Observability/">
|
<Folder Name="/Samples/03-workflows/Observability/">
|
||||||
<Project Path="samples/03-workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
|
<Project Path="samples/03-workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
|
||||||
@@ -341,6 +344,9 @@
|
|||||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/">
|
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/">
|
||||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj" />
|
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/">
|
||||||
|
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/">
|
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/">
|
||||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj" />
|
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
@@ -357,6 +363,9 @@
|
|||||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
||||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
|
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/">
|
||||||
|
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/Samples/04-hosting/DurableAgents/" />
|
<Folder Name="/Samples/04-hosting/DurableAgents/" />
|
||||||
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
||||||
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
|
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
|
||||||
@@ -592,15 +601,17 @@
|
|||||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj" />
|
|
||||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||||
|
<Project Path="src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.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.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.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||||
|
<Project Path="src/Microsoft.Agents.AI.Mcp/Microsoft.Agents.AI.Mcp.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||||
@@ -618,8 +629,8 @@
|
|||||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
|
|
||||||
<Project Path="tests/Foundry.Hosting.IntegrationTests.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.csproj" />
|
<Project Path="tests/Foundry.Hosting.IntegrationTests.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.csproj" />
|
||||||
|
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||||
@@ -644,8 +655,8 @@
|
|||||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj" />
|
||||||
|
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||||
@@ -654,6 +665,7 @@
|
|||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
|
||||||
|
<Project Path="tests/Microsoft.Agents.AI.Mcp.UnitTests/Microsoft.Agents.AI.Mcp.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
||||||
|
|||||||
@@ -20,13 +20,17 @@
|
|||||||
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
|
||||||
|
"src\\Microsoft.Agents.AI.Hosting.AspNetCore\\Microsoft.Agents.AI.Hosting.AspNetCore.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
|
"src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
|
"src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
|
||||||
|
"src\\Microsoft.Agents.AI.Mcp\\Microsoft.Agents.AI.Mcp.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
||||||
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
|
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
||||||
|
"src\\Microsoft.Agents.AI.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
|
||||||
|
"src\\Microsoft.Agents.AI.Workflows.Declarative.Mcp\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
|
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -363,6 +363,25 @@ internal static class AgentsSamples
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
new SampleDefinition
|
||||||
|
{
|
||||||
|
Name = "Agent_Step06_McpBasedSkills",
|
||||||
|
ProjectPath = "samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills",
|
||||||
|
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||||
|
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||||
|
MustContain =
|
||||||
|
[
|
||||||
|
"Discovering MCP-based skills",
|
||||||
|
"Agent:",
|
||||||
|
],
|
||||||
|
ExpectedOutputDescription =
|
||||||
|
[
|
||||||
|
"The output should show the agent converting 26.2 miles to kilometers and 75 kilograms to pounds.",
|
||||||
|
"The response should contain approximate numeric values for both conversions.",
|
||||||
|
"The output should not contain error messages or stack traces.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// ── AgentWithMemory ─────────────────────────────────────────────────
|
// ── AgentWithMemory ─────────────────────────────────────────────────
|
||||||
|
|
||||||
new SampleDefinition
|
new SampleDefinition
|
||||||
@@ -1151,6 +1170,25 @@ internal static class AgentsSamples
|
|||||||
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
|
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
new SampleDefinition
|
||||||
|
{
|
||||||
|
Name = "Agent_MCP_LongRunningTask_Client",
|
||||||
|
ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client",
|
||||||
|
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||||
|
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||||
|
MustContain =
|
||||||
|
[
|
||||||
|
"=== Transparent long-running MCP task (RunAsync) ===",
|
||||||
|
"=== Transparent long-running MCP task (RunStreamingAsync) ===",
|
||||||
|
],
|
||||||
|
ExpectedOutputDescription =
|
||||||
|
[
|
||||||
|
"The output should show an agent analyzing a dataset named 'sales-2025-q1' and producing a summary mentioning rows, revenue, anomalies, or outliers.",
|
||||||
|
"The output should contain both a non-streaming response (after RunAsync) and a streaming response (after RunStreamingAsync) for the same analysis question.",
|
||||||
|
"The output should not contain error messages or stack traces.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
new SampleDefinition
|
new SampleDefinition
|
||||||
{
|
{
|
||||||
Name = "AGUI_Step01_GettingStarted_Client",
|
Name = "AGUI_Step01_GettingStarted_Client",
|
||||||
|
|||||||
@@ -439,15 +439,6 @@ internal static class WorkflowSamples
|
|||||||
ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
|
ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
|
||||||
},
|
},
|
||||||
|
|
||||||
new SampleDefinition
|
|
||||||
{
|
|
||||||
Name = "Workflow_Declarative_GenerateCode",
|
|
||||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
|
||||||
IsDeterministic = true,
|
|
||||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
|
||||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
|
||||||
},
|
|
||||||
|
|
||||||
new SampleDefinition
|
new SampleDefinition
|
||||||
{
|
{
|
||||||
Name = "Workflow_Declarative_HostedWorkflow",
|
Name = "Workflow_Declarative_HostedWorkflow",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Central version prefix - applies to all nuget packages. -->
|
<!-- Central version prefix - applies to all nuget packages. -->
|
||||||
<VersionPrefix>1.6.2</VersionPrefix>
|
<VersionPrefix>1.9.0</VersionPrefix>
|
||||||
<RCNumber>1</RCNumber>
|
<RCNumber>1</RCNumber>
|
||||||
<DateSuffix>260521</DateSuffix>
|
<DateSuffix>260603</DateSuffix>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
<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)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||||
<GitTag>1.6.2</GitTag>
|
<GitTag>1.9.0</GitTag>
|
||||||
|
|
||||||
<Configurations>Debug;Release;Publish</Configurations>
|
<Configurations>Debug;Release;Publish</Configurations>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddHttpClient().AddLogging();
|
builder.Services.AddHttpClient().AddLogging();
|
||||||
builder.Services.AddAGUI();
|
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
|
||||||
|
// deployments, e.g.:
|
||||||
|
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
|||||||
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
|
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
|
||||||
builder.Services.AddAGUI();
|
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
|
||||||
|
// deployments, e.g.:
|
||||||
|
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddHttpClient().AddLogging();
|
builder.Services.AddHttpClient().AddLogging();
|
||||||
builder.Services.AddAGUI();
|
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
|
||||||
|
// deployments, e.g.:
|
||||||
|
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
|||||||
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
|
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
|
||||||
builder.Services.AddAGUI();
|
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
|
||||||
|
// deployments, e.g.:
|
||||||
|
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
app.UseHttpLogging();
|
app.UseHttpLogging();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ builder.Services.AddAGUI();
|
|||||||
// Configure to listen on port 8888
|
// Configure to listen on port 8888
|
||||||
builder.WebHost.UseUrls("http://localhost:8888");
|
builder.WebHost.UseUrls("http://localhost:8888");
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// deployments, e.g.:
|
||||||
|
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
|||||||
var skillsProvider = new AgentSkillsProvider(
|
var skillsProvider = new AgentSkillsProvider(
|
||||||
Path.Combine(AppContext.BaseDirectory, "skills"),
|
Path.Combine(AppContext.BaseDirectory, "skills"),
|
||||||
SubprocessScriptRunner.RunAsync);
|
SubprocessScriptRunner.RunAsync);
|
||||||
|
|
||||||
// --- Agent Setup ---
|
// --- Agent Setup ---
|
||||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||||
.GetResponsesClient()
|
.GetResponsesClient()
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Console.WriteLine($"Agent: {response.Text}");
|
|||||||
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
||||||
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
||||||
/// are automatically discovered as skill scripts. Alternatively,
|
/// are automatically discovered as skill scripts. Alternatively,
|
||||||
/// <see cref="AgentSkill.Resources"/> and <see cref="AgentSkill.Scripts"/> can be overridden.
|
/// <see cref="AgentClassSkill{TSelf}.Resources"/> and <see cref="AgentClassSkill{TSelf}.Scripts"/> can be overridden.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
|
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
|
||||||
{
|
{
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||||
|
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<NoWarn>$(NoWarn);MAAI001;MCPEXP001</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.AI.OpenAI" />
|
||||||
|
<PackageReference Include="Azure.Identity" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
// This sample demonstrates how to discover Agent Skills served over MCP.
|
||||||
|
//
|
||||||
|
// When launched with "--server", this executable runs a small MCP stdio server
|
||||||
|
// that exposes a unit-converter skill via the SEP-2640 convention:
|
||||||
|
// - skill://index.json — discovery document listing all skills
|
||||||
|
// - skill://unit-converter/SKILL.md — the skill instructions
|
||||||
|
//
|
||||||
|
// In default (client) mode the sample launches itself as a child process,
|
||||||
|
// connects via StdioClientTransport, and uses AgentSkillsProviderBuilder
|
||||||
|
// to discover and inject the skill into a ChatClientAgent.
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Azure.AI.OpenAI;
|
||||||
|
using Azure.Identity;
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ModelContextProtocol.Client;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using OpenAI.Responses;
|
||||||
|
|
||||||
|
if (args.Length > 0 && args[0] == "--server")
|
||||||
|
{
|
||||||
|
await RunMcpServerAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
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.
|
||||||
|
var thisAssemblyPath = typeof(Program).Assembly.Location;
|
||||||
|
Console.WriteLine("Discovering MCP-based skills");
|
||||||
|
|
||||||
|
await using McpClient client = await McpClient.CreateAsync(
|
||||||
|
new StdioClientTransport(new()
|
||||||
|
{
|
||||||
|
Name = "skills-server",
|
||||||
|
Command = "dotnet",
|
||||||
|
Arguments = [thisAssemblyPath, "--server"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
var skillsProvider = new AgentSkillsProviderBuilder()
|
||||||
|
.UseMcpSkills(client)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// --- Agent ---
|
||||||
|
// 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(openAiEndpoint), new DefaultAzureCredential())
|
||||||
|
.GetResponsesClient()
|
||||||
|
.AsAIAgent(new ChatClientAgentOptions
|
||||||
|
{
|
||||||
|
Name = "SkillsAgent",
|
||||||
|
ChatOptions = new()
|
||||||
|
{
|
||||||
|
Instructions = "You are a helpful assistant. Use available skills to answer the user.",
|
||||||
|
},
|
||||||
|
AIContextProviders = [skillsProvider],
|
||||||
|
},
|
||||||
|
model: deploymentName);
|
||||||
|
|
||||||
|
// --- Run ---
|
||||||
|
Console.WriteLine(new string('-', 60));
|
||||||
|
|
||||||
|
AgentResponse response = await agent.RunAsync(
|
||||||
|
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
|
||||||
|
|
||||||
|
Console.WriteLine($"Agent: {response.Text}");
|
||||||
|
|
||||||
|
// --- Server mode (launched as a child process via --server) ---------------------------------
|
||||||
|
static async Task RunMcpServerAsync()
|
||||||
|
{
|
||||||
|
var builder = Host.CreateApplicationBuilder();
|
||||||
|
|
||||||
|
// Critical for stdio transport: any provider that writes to stdout will corrupt the
|
||||||
|
// JSON-RPC channel. Clear all providers; the MCP SDK routes its own diagnostics
|
||||||
|
// appropriately.
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
|
||||||
|
|
||||||
|
builder.Services.AddMcpServer(o => o.ServerInfo = new() { Name = "SkillsServer", Version = "1.0.0" })
|
||||||
|
.WithStdioServerTransport()
|
||||||
|
.WithResources<SkillResources>();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1812 // Discovered by MCP SDK via [McpServerResourceType] attribute
|
||||||
|
[McpServerResourceType]
|
||||||
|
internal sealed class SkillResources
|
||||||
|
#pragma warning restore CA1812
|
||||||
|
{
|
||||||
|
private const string IndexJson = """
|
||||||
|
{
|
||||||
|
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "unit-converter",
|
||||||
|
"type": "skill-md",
|
||||||
|
"description": "Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.",
|
||||||
|
"url": "skill://unit-converter/SKILL.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string SkillMd = """
|
||||||
|
---
|
||||||
|
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, use these factors:
|
||||||
|
|
||||||
|
| From | To | Factor |
|
||||||
|
|-------------|-------------|----------|
|
||||||
|
| miles | kilometers | 1.60934 |
|
||||||
|
| kilometers | miles | 0.621371 |
|
||||||
|
| pounds | kilograms | 0.453592 |
|
||||||
|
| kilograms | pounds | 2.20462 |
|
||||||
|
|
||||||
|
Formula: result = value Ă— factor
|
||||||
|
""";
|
||||||
|
|
||||||
|
[McpServerResource(UriTemplate = "skill://index.json", Name = "Skill Index", MimeType = "application/json")]
|
||||||
|
[Description("SEP-2640 skill discovery index")]
|
||||||
|
public static string GetIndex() => IndexJson;
|
||||||
|
|
||||||
|
[McpServerResource(UriTemplate = "skill://unit-converter/SKILL.md", Name = "Unit Converter Skill", MimeType = "text/markdown")]
|
||||||
|
[Description("Unit converter skill instructions")]
|
||||||
|
public static string GetSkillMd() => SkillMd;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# MCP-Based Agent Skills Sample
|
||||||
|
|
||||||
|
This sample demonstrates how to discover **Agent Skills served over MCP** with a `ChatClientAgent`.
|
||||||
|
|
||||||
|
## What it demonstrates
|
||||||
|
|
||||||
|
- Hosting a small MCP server (in this same executable, launched with `--server`) that
|
||||||
|
exposes skill resources following the SEP-2640 convention.
|
||||||
|
- Connecting an `McpClient` to the embedded server via stdio transport.
|
||||||
|
- Building an `AgentSkillsProvider` via `UseMcpSkills(client)`, which reads
|
||||||
|
`skill://index.json` (SEP-2640 canonical discovery) and constructs skills from the
|
||||||
|
index entries.
|
||||||
|
- The progressive disclosure pattern across MCP: advertise → load → read resources, exactly
|
||||||
|
as for filesystem-backed skills.
|
||||||
|
|
||||||
|
## Running the Sample
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- .NET 10.0 SDK
|
||||||
|
- Azure OpenAI endpoint with a deployed model
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||||
|
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
@@ -9,6 +9,7 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w
|
|||||||
| [Agent_Step03_ClassBasedSkills](Agent_Step03_ClassBasedSkills/) | Define skills as C# classes using `AgentClassSkill`. |
|
| [Agent_Step03_ClassBasedSkills](Agent_Step03_ClassBasedSkills/) | Define skills as C# classes using `AgentClassSkill`. |
|
||||||
| [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) | **(Advanced)** Combine file-based, code-defined, and class-based skills using `AgentSkillsProviderBuilder`. |
|
| [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) | **(Advanced)** Combine file-based, code-defined, and class-based skills using `AgentSkillsProviderBuilder`. |
|
||||||
| [Agent_Step05_SkillsWithDI](Agent_Step05_SkillsWithDI/) | Use Dependency Injection with both code-defined (`AgentInlineSkill`) and class-based (`AgentClassSkill`) skills. |
|
| [Agent_Step05_SkillsWithDI](Agent_Step05_SkillsWithDI/) | Use Dependency Injection with both code-defined (`AgentInlineSkill`) and class-based (`AgentClassSkill`) skills. |
|
||||||
|
| [Agent_Step06_McpBasedSkills](Agent_Step06_McpBasedSkills/) | Discover skills served over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) via `AgentMcpSkillsSource`. Spins up an in-process MCP server that exposes skills as resources (`skill://...`) and connects an `McpClient` to it. |
|
||||||
|
|
||||||
## Key Concepts
|
## Key Concepts
|
||||||
|
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||||
|
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
|
<PackageReference Include="Azure.Identity" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
// Foundry Toolbox MCP Skills.
|
||||||
|
//
|
||||||
|
// Uses AgentSkillsProviderBuilder to discover MCP-based skills from a Foundry
|
||||||
|
// Toolbox endpoint and inject them as AIContextProviders so the agent can
|
||||||
|
// discover and use them at runtime.
|
||||||
|
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Azure.AI.Projects;
|
||||||
|
using Azure.Core;
|
||||||
|
using Azure.Identity;
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using ModelContextProtocol.Client;
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||||
|
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||||
|
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||||
|
string toolboxMcpServerUrl = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_MCP_SERVER_URL")
|
||||||
|
?? throw new InvalidOperationException("FOUNDRY_TOOLBOX_MCP_SERVER_URL is not set.");
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
TokenCredential credential = new DefaultAzureCredential();
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient(new BearerTokenHandler(credential, "https://ai.azure.com/.default")
|
||||||
|
{
|
||||||
|
InnerHandler = new HttpClientHandler(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Connect to the Foundry Toolbox MCP endpoint ---
|
||||||
|
await using McpClient mcpClient = await McpClient.CreateAsync(
|
||||||
|
new HttpClientTransport(
|
||||||
|
new HttpClientTransportOptions
|
||||||
|
{
|
||||||
|
Endpoint = new Uri(toolboxMcpServerUrl),
|
||||||
|
Name = "foundry_toolbox",
|
||||||
|
TransportMode = HttpTransportMode.StreamableHttp,
|
||||||
|
AdditionalHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["Foundry-Features"] = "Toolboxes=V1Preview",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
httpClient));
|
||||||
|
|
||||||
|
// --- Discover MCP-based skills ---
|
||||||
|
var skillsProvider = new AgentSkillsProviderBuilder()
|
||||||
|
.UseMcpSkills(mcpClient)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// --- Create the agent ---
|
||||||
|
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||||
|
|
||||||
|
AIAgent agent = aiProjectClient.AsAIAgent(
|
||||||
|
options: new ChatClientAgentOptions
|
||||||
|
{
|
||||||
|
Name = "ToolboxMcpSkillsAgent",
|
||||||
|
ChatOptions = new()
|
||||||
|
{
|
||||||
|
ModelId = deploymentName,
|
||||||
|
Instructions = "You are a helpful assistant. Use available skills to answer the user.",
|
||||||
|
},
|
||||||
|
AIContextProviders = [skillsProvider],
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Interactive prompt ---
|
||||||
|
Console.Write("User: ");
|
||||||
|
string? query = Console.ReadLine();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
{
|
||||||
|
Console.WriteLine("No input provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Assistant: {await agent.RunAsync(query)}");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DelegatingHandler: attaches a fresh Foundry bearer token to every request
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
internal sealed class BearerTokenHandler(TokenCredential credential, string scope) : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly TokenRequestContext _tokenContext = new([scope]);
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
AccessToken token = await credential.GetTokenAsync(this._tokenContext, cancellationToken).ConfigureAwait(false);
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
|
||||||
|
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Foundry Toolbox MCP Skills
|
||||||
|
|
||||||
|
This sample uses
|
||||||
|
`AgentSkillsProviderBuilder` to discover MCP-based skills from a Foundry Toolbox endpoint
|
||||||
|
and inject them as `AIContextProviders` so the agent can discover and use them at runtime.
|
||||||
|
|
||||||
|
## What this sample demonstrates
|
||||||
|
|
||||||
|
- Connecting to a Foundry toolbox's MCP endpoint via Streamable HTTP transport
|
||||||
|
- Injecting a fresh Azure AI bearer token (`https://ai.azure.com/.default`) on every MCP request
|
||||||
|
- Using `AgentSkillsProviderBuilder.UseMcpSkills(client)` to discover skills from the toolbox
|
||||||
|
- Injecting the discovered skills into `AIProjectClient.AsAIAgent(...)` via `AIContextProviders`
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Microsoft Foundry project with a toolbox already configured
|
||||||
|
- The toolbox MCP endpoint must expose `skill://index.json` with `skill-md` entries (SEP-2640). If the resource is absent, the sample runs but the skills provider will be empty.
|
||||||
|
- Azure CLI installed and authenticated (`az login`)
|
||||||
|
|
||||||
|
Set the following environment variables:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
|
||||||
|
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini"
|
||||||
|
$env:FOUNDRY_TOOLBOX_MCP_SERVER_URL="https://your-foundry-service.services.ai.azure.com/api/projects/your-project/toolboxes/your-toolbox/mcp?api-version=v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the sample
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
@@ -74,6 +74,7 @@ Some samples require extra tool-specific environment variables. See each sample
|
|||||||
| [Local MCP](./Agent_Step23_LocalMCP/) | Local MCP client with HTTP transport |
|
| [Local MCP](./Agent_Step23_LocalMCP/) | Local MCP client with HTTP transport |
|
||||||
| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
|
| [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter |
|
||||||
| [Foundry toolbox via MCP](./Agent_Step25_FoundryToolboxMcp/) | Use a Foundry Toolbox from a non-hosted agent via its MCP endpoint |
|
| [Foundry toolbox via MCP](./Agent_Step25_FoundryToolboxMcp/) | Use a Foundry Toolbox from a non-hosted agent via its MCP endpoint |
|
||||||
|
| [Foundry toolbox MCP skills](./Agent_Step26_FoundryToolboxMcpSkills/) | Use a Foundry Toolbox with MCP-based skills discovery (SEP-2640) via AIContextProviders |
|
||||||
|
|
||||||
## Running the samples
|
## Running the samples
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,95 @@ public static class AnsiEscapes
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string ResetAttributes => "\x1b[0m";
|
public static string ResetAttributes => "\x1b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the visible (printed) length of a string after stripping ANSI escape sequences.
|
||||||
|
/// Escape sequences are zero-width on screen but occupy characters in the raw string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This counts UTF-16 code units (chars) rather than terminal display cells. Emoji,
|
||||||
|
/// combining characters, variation selectors, and East Asian wide characters may be
|
||||||
|
/// measured incorrectly. For the console harness this is acceptable since content is
|
||||||
|
/// predominantly ASCII, and emoji are padded with surrounding spaces.
|
||||||
|
/// </remarks>
|
||||||
|
public static int VisibleLength(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = 0;
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
if (text[i] == '\x1b' && i + 1 < text.Length && text[i + 1] == '[')
|
||||||
|
{
|
||||||
|
// Skip the ESC[ and all characters up to and including the final byte (0x40–0x7E).
|
||||||
|
i += 2;
|
||||||
|
while (i < text.Length && text[i] < 0x40)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// i now points to the final byte of the escape sequence; the for-loop will advance past it.
|
||||||
|
}
|
||||||
|
else if (text[i] != '\n' && text[i] != '\r')
|
||||||
|
{
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts the number of physical terminal rows a text item will occupy,
|
||||||
|
/// accounting for both explicit newlines and terminal line wrapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to measure.</param>
|
||||||
|
/// <param name="terminalWidth">The terminal width in columns. If <= 0, wrapping is ignored (1 row per logical line).</param>
|
||||||
|
/// <returns>The number of physical rows the text occupies.</returns>
|
||||||
|
public static int CountPhysicalLines(string text, int terminalWidth)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int physicalLines = 0;
|
||||||
|
int lineStart = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i <= text.Length; i++)
|
||||||
|
{
|
||||||
|
if (i == text.Length || text[i] == '\n')
|
||||||
|
{
|
||||||
|
if (terminalWidth <= 0)
|
||||||
|
{
|
||||||
|
// No wrapping — each logical line is one physical row
|
||||||
|
physicalLines += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string logicalLine = text[lineStart..i];
|
||||||
|
int visibleWidth = VisibleLength(logicalLine);
|
||||||
|
|
||||||
|
physicalLines += visibleWidth == 0
|
||||||
|
? 1
|
||||||
|
: (visibleWidth - 1) / terminalWidth + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If text ends with a newline, don't count the trailing empty line
|
||||||
|
if (text[text.Length - 1] == '\n')
|
||||||
|
{
|
||||||
|
physicalLines--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return physicalLines;
|
||||||
|
}
|
||||||
|
|
||||||
private static int ConsoleColorToAnsi(ConsoleColor color) => color switch
|
private static int ConsoleColorToAnsi(ConsoleColor color) => color switch
|
||||||
{
|
{
|
||||||
ConsoleColor.Black => 30,
|
ConsoleColor.Black => 30,
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
foreach (string line in props.Title.Split('\n'))
|
foreach (string line in props.Title.Split('\n'))
|
||||||
{
|
{
|
||||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
||||||
Console.Write(AnsiEscapes.EraseEntireLine);
|
|
||||||
Console.Write(line);
|
Console.Write(line);
|
||||||
|
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
for (int i = 0; i < totalItems; i++)
|
for (int i = 0; i < totalItems; i++)
|
||||||
{
|
{
|
||||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
||||||
Console.Write(AnsiEscapes.EraseEntireLine);
|
|
||||||
|
|
||||||
bool isSelected = i == props.SelectedIndex;
|
bool isSelected = i == props.SelectedIndex;
|
||||||
bool isCustomTextOption = props.CustomTextPlaceholder != null && i == props.Items.Count;
|
bool isCustomTextOption = props.CustomTextPlaceholder != null && i == props.Items.Count;
|
||||||
@@ -72,6 +71,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.Write(props.Items[i]);
|
Console.Write(props.Items[i]);
|
||||||
|
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||||
|
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
{
|
{
|
||||||
@@ -101,6 +101,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.Write(props.CustomText);
|
Console.Write(props.CustomText);
|
||||||
|
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||||
|
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
{
|
{
|
||||||
@@ -121,6 +122,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
|
|
||||||
Console.Write(" ");
|
Console.Write(" ");
|
||||||
Console.Write(props.CustomTextPlaceholder);
|
Console.Write(props.CustomTextPlaceholder);
|
||||||
|
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||||
Console.Write(AnsiEscapes.ResetAttributes);
|
Console.Write(AnsiEscapes.ResetAttributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,16 +23,18 @@ public record TextPanelProps : ConsoleReactiveProps
|
|||||||
public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiveState>
|
public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiveState>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the height (in lines) needed to render all items.
|
/// Calculates the height (in lines) needed to render all items,
|
||||||
|
/// accounting for terminal line wrapping at the specified width.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="items">The items to measure.</param>
|
/// <param name="items">The items to measure.</param>
|
||||||
/// <returns>The total number of lines all items will occupy.</returns>
|
/// <param name="terminalWidth">The terminal width in columns. When 0 or negative, wrapping is ignored.</param>
|
||||||
public static int CalculateHeight(IReadOnlyList<string> items)
|
/// <returns>The total number of physical lines all items will occupy.</returns>
|
||||||
|
public static int CalculateHeight(IReadOnlyList<string> items, int terminalWidth = 0)
|
||||||
{
|
{
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (int i = 0; i < items.Count; i++)
|
for (int i = 0; i < items.Count; i++)
|
||||||
{
|
{
|
||||||
total += CountLines(items[i]);
|
total += AnsiEscapes.CountPhysicalLines(items[i], terminalWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
return total;
|
return total;
|
||||||
@@ -47,13 +49,20 @@ public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiv
|
|||||||
{
|
{
|
||||||
string text = props.Items[i];
|
string text = props.Items[i];
|
||||||
string[] lines = text.Split('\n');
|
string[] lines = text.Split('\n');
|
||||||
int lineCount = CountLines(text);
|
int itemLineCount = AnsiEscapes.CountPhysicalLines(text, props.Width);
|
||||||
|
int itemRow = 0;
|
||||||
|
|
||||||
for (int j = 0; j < lineCount; j++)
|
for (int j = 0; j < lines.Length && itemRow < itemLineCount; j++)
|
||||||
{
|
{
|
||||||
|
int linePhysicalRows = props.Width > 0
|
||||||
|
? Math.Max(1, (AnsiEscapes.VisibleLength(lines[j]) - 1) / props.Width + 1)
|
||||||
|
: 1;
|
||||||
|
|
||||||
Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + currentRow));
|
Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + currentRow));
|
||||||
Console.Write(lines[j]);
|
Console.Write(lines[j]);
|
||||||
currentRow++;
|
|
||||||
|
currentRow += linePhysicalRows;
|
||||||
|
itemRow += linePhysicalRows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,29 +75,4 @@ public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountLines(string text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = 1;
|
|
||||||
for (int i = 0; i < text.Length; i++)
|
|
||||||
{
|
|
||||||
if (text[i] == '\n')
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If text ends with a newline, don't count the trailing empty line
|
|
||||||
if (text[text.Length - 1] == '\n')
|
|
||||||
{
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,19 @@ public record TextScrollPanelProps : ConsoleReactiveProps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// State for <see cref="TextScrollPanel"/>.
|
/// State for <see cref="TextScrollPanel"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="RenderedCount">The number of items already rendered.</param>
|
public record TextScrollPanelState : ConsoleReactiveState;
|
||||||
public record TextScrollPanelState(int RenderedCount = 0) : ConsoleReactiveState;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A component that renders pre-rendered string items within a scroll area.
|
/// A component that renders pre-rendered string items within a scroll area.
|
||||||
/// All items are considered finalized — only new items since the last render are output.
|
/// The last rendered item is considered dynamic and will be re-rendered on each call.
|
||||||
/// Use <see cref="Reset"/> to force a full re-render.
|
/// All prior items are considered finalized and are not re-rendered.
|
||||||
|
/// Use <see cref="Invalidate"/> to force a full re-render.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, TextScrollPanelState>
|
public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, TextScrollPanelState>
|
||||||
{
|
{
|
||||||
|
private int _renderedCount;
|
||||||
|
private int _lastItemOffsetFromBottom;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TextScrollPanel"/> class.
|
/// Initializes a new instance of the <see cref="TextScrollPanel"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,12 +38,12 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
|
|||||||
this.State = new TextScrollPanelState();
|
this.State = new TextScrollPanelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Resets the panel so all items will be re-rendered on the next Render call.
|
public override void Invalidate()
|
||||||
/// </summary>
|
|
||||||
public void Reset()
|
|
||||||
{
|
{
|
||||||
this.State = new TextScrollPanelState();
|
this._renderedCount = 0;
|
||||||
|
this._lastItemOffsetFromBottom = 0;
|
||||||
|
base.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -51,16 +54,35 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move cursor to the bottom of the scroll area
|
int bottomRow = props.Y + props.Height - 1;
|
||||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + props.Height - 1, props.X));
|
|
||||||
|
|
||||||
// Output only new items since last rendered
|
// Determine the first item to render. If we previously rendered items,
|
||||||
for (int i = state.RenderedCount; i < props.Items.Count; i++)
|
// re-render the last one (it may have changed/grown) from its stored position.
|
||||||
|
int startIndex = this._renderedCount > 0 ? this._renderedCount - 1 : 0;
|
||||||
|
|
||||||
|
if (this._renderedCount > 0 && this._lastItemOffsetFromBottom > 0)
|
||||||
|
{
|
||||||
|
// Reposition cursor to where the last rendered item began
|
||||||
|
Console.Write(AnsiEscapes.MoveCursor(bottomRow - this._lastItemOffsetFromBottom, props.X));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// First render — position at the bottom of the scroll area
|
||||||
|
Console.Write(AnsiEscapes.MoveCursor(bottomRow, props.X));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render from startIndex onwards
|
||||||
|
for (int i = startIndex; i < props.Items.Count; i++)
|
||||||
{
|
{
|
||||||
Console.Write(props.Items[i]);
|
Console.Write(props.Items[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state to track what we've rendered
|
// Calculate the offset from bottom for the start of the new last item,
|
||||||
this.State = new TextScrollPanelState(props.Items.Count);
|
// accounting for terminal line wrapping at the available width.
|
||||||
|
int lastItemLines = AnsiEscapes.CountPhysicalLines(props.Items[^1], props.Width);
|
||||||
|
this._lastItemOffsetFromBottom = lastItemLines > 0 ? lastItemLines - 1 : 0;
|
||||||
|
|
||||||
|
// Update rendered count
|
||||||
|
this._renderedCount = props.Items.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -13,6 +13,11 @@ public abstract class ConsoleReactiveComponent
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the shared render lock across all component types to prevent ANSI escape sequence interleaving.
|
||||||
|
/// </summary>
|
||||||
|
protected static object RenderLock { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the component's props as the base <see cref="ConsoleReactiveProps"/> type.
|
/// Gets or sets the component's props as the base <see cref="ConsoleReactiveProps"/> type.
|
||||||
/// Used by parent components to set layout (X, Y, Width, Height) on children without
|
/// Used by parent components to set layout (X, Y, Width, Height) on children without
|
||||||
@@ -40,7 +45,6 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
|
|||||||
where TProps : ConsoleReactiveProps
|
where TProps : ConsoleReactiveProps
|
||||||
where TState : ConsoleReactiveState
|
where TState : ConsoleReactiveState
|
||||||
{
|
{
|
||||||
private readonly object _renderLock = new();
|
|
||||||
private TProps? _lastRenderedProps;
|
private TProps? _lastRenderedProps;
|
||||||
private TState? _lastRenderedState;
|
private TState? _lastRenderedState;
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public override void Render()
|
public override void Render()
|
||||||
{
|
{
|
||||||
lock (this._renderLock)
|
lock (RenderLock)
|
||||||
{
|
{
|
||||||
if (this.Props is null)
|
if (this.Props is null)
|
||||||
{
|
{
|
||||||
@@ -97,7 +101,7 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void Invalidate()
|
public override void Invalidate()
|
||||||
{
|
{
|
||||||
lock (this._renderLock)
|
lock (RenderLock)
|
||||||
{
|
{
|
||||||
this._lastRenderedProps = default;
|
this._lastRenderedProps = default;
|
||||||
this._lastRenderedState = default;
|
this._lastRenderedState = default;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
namespace Harness.ConsoleReactiveFramework;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Caches the result of a mapping function and only recomputes when the input changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInput">The type of the input value.</typeparam>
|
||||||
|
/// <typeparam name="TOutput">The type of the mapped output value.</typeparam>
|
||||||
|
public class ConsoleReactiveMemo<TInput, TOutput>
|
||||||
|
{
|
||||||
|
private TInput? _previousInput;
|
||||||
|
private TOutput? _cachedOutput;
|
||||||
|
private bool _hasValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached output if <paramref name="input"/> equals the previously stored input;
|
||||||
|
/// otherwise invokes <paramref name="mapper"/> to compute and cache a new output.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The current input value.</param>
|
||||||
|
/// <param name="mapper">A function that maps the input to an output value.</param>
|
||||||
|
/// <returns>The cached or newly computed output.</returns>
|
||||||
|
public TOutput Map(TInput input, Func<TInput, TOutput> mapper)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(mapper);
|
||||||
|
|
||||||
|
if (!this._hasValue || !EqualityComparer<TInput>.Default.Equals(input, this._previousInput))
|
||||||
|
{
|
||||||
|
this._previousInput = input;
|
||||||
|
this._cachedOutput = mapper(input);
|
||||||
|
this._hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._cachedOutput!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
private readonly ListSelection _listSelection = new();
|
private readonly ListSelection _listSelection = new();
|
||||||
private readonly TextInput _textInput = new();
|
private readonly TextInput _textInput = new();
|
||||||
private readonly TextScrollPanel _textScrollPanel = new();
|
private readonly TextScrollPanel _textScrollPanel = new();
|
||||||
private readonly TextPanel _textPanel = new();
|
|
||||||
private readonly TextPanel _queuedPanel = new();
|
private readonly TextPanel _queuedPanel = new();
|
||||||
private readonly AgentStatus _agentStatus = new();
|
private readonly AgentStatus _agentStatus = new();
|
||||||
private readonly AgentModeAndHelp _modeAndHelp = new();
|
private readonly AgentModeAndHelp _modeAndHelp = new();
|
||||||
@@ -29,6 +28,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
private int _scrollRegionBottom;
|
private int _scrollRegionBottom;
|
||||||
private bool _resizedSinceLastRender = true;
|
private bool _resizedSinceLastRender = true;
|
||||||
private bool _deactivated;
|
private bool _deactivated;
|
||||||
|
private BottomPanelMode _lastRenderedBottomPanelMode;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="HarnessAppComponent"/> class.
|
/// Initializes a new instance of the <see cref="HarnessAppComponent"/> class.
|
||||||
@@ -341,18 +341,8 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the text panel height for the last scroll item
|
|
||||||
IReadOnlyList<string> lastItems = state.ScrollAreaContentItems.Count > 0
|
|
||||||
? [state.ScrollAreaContentItems[^1]]
|
|
||||||
: [];
|
|
||||||
int textPanelHeight = TextPanel.CalculateHeight(lastItems);
|
|
||||||
if (textPanelHeight > 0)
|
|
||||||
{
|
|
||||||
textPanelHeight++; // Extra line for spacing between text panel and rule
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate queued items panel height
|
// Calculate queued items panel height
|
||||||
int queuedPanelHeight = TextPanel.CalculateHeight(state.QueuedItems);
|
int queuedPanelHeight = TextPanel.CalculateHeight(state.QueuedItems, state.ConsoleWidth);
|
||||||
|
|
||||||
// Build the bottom panel child based on mode
|
// Build the bottom panel child based on mode
|
||||||
ConsoleReactiveComponent bottomChild;
|
ConsoleReactiveComponent bottomChild;
|
||||||
@@ -417,6 +407,14 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
bottomChild = this._textInput;
|
bottomChild = this._textInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the bottom panel mode changes, the new child must repaint even if its
|
||||||
|
// props haven't changed — the screen area was overwritten by the previous child.
|
||||||
|
if (state.Mode != this._lastRenderedBottomPanelMode)
|
||||||
|
{
|
||||||
|
bottomChild.Invalidate();
|
||||||
|
this._lastRenderedBottomPanelMode = state.Mode;
|
||||||
|
}
|
||||||
|
|
||||||
var ruleProps = new TopBottomRuleProps
|
var ruleProps = new TopBottomRuleProps
|
||||||
{
|
{
|
||||||
Width = state.ConsoleWidth,
|
Width = state.ConsoleWidth,
|
||||||
@@ -444,7 +442,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
int modeAndHelpHeight = showStatusAndHelp ? AgentModeAndHelp.CalculateHeight(modeAndHelpProps) : 0;
|
int modeAndHelpHeight = showStatusAndHelp ? AgentModeAndHelp.CalculateHeight(modeAndHelpProps) : 0;
|
||||||
|
|
||||||
int ruleHeight = TopBottomRule.CalculateHeight(ruleProps);
|
int ruleHeight = TopBottomRule.CalculateHeight(ruleProps);
|
||||||
int nonScrollHeight = ruleHeight + textPanelHeight + agentStatusHeight + queuedPanelHeight + modeAndHelpHeight + 1; // +1 for bottom padding
|
int nonScrollHeight = ruleHeight + agentStatusHeight + queuedPanelHeight + modeAndHelpHeight + 1; // +1 for bottom padding
|
||||||
int scrollBottom = Math.Max(1, state.ConsoleHeight - nonScrollHeight);
|
int scrollBottom = Math.Max(1, state.ConsoleHeight - nonScrollHeight);
|
||||||
|
|
||||||
// If scroll region changed or a clear is needed, reset everything
|
// If scroll region changed or a clear is needed, reset everything
|
||||||
@@ -455,52 +453,36 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
System.Console.Write(AnsiEscapes.ResetScrollRegion);
|
System.Console.Write(AnsiEscapes.ResetScrollRegion);
|
||||||
System.Console.Write(AnsiEscapes.EraseEntireScreen);
|
System.Console.Write(AnsiEscapes.EraseEntireScreen);
|
||||||
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
|
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
|
||||||
this._textScrollPanel.Reset();
|
|
||||||
this._resizedSinceLastRender = false;
|
|
||||||
|
|
||||||
// Invalidate all children so they re-render even if props haven't changed
|
// Invalidate all children so they re-render even if props haven't changed
|
||||||
this._rule.Invalidate();
|
this._rule.Invalidate();
|
||||||
this._textScrollPanel.Invalidate();
|
this._textScrollPanel.Invalidate();
|
||||||
this._textPanel.Invalidate();
|
|
||||||
this._queuedPanel.Invalidate();
|
this._queuedPanel.Invalidate();
|
||||||
this._agentStatus.Invalidate();
|
this._agentStatus.Invalidate();
|
||||||
this._modeAndHelp.Invalidate();
|
this._modeAndHelp.Invalidate();
|
||||||
this._textInput.Invalidate();
|
this._textInput.Invalidate();
|
||||||
this._listSelection.Invalidate();
|
this._listSelection.Invalidate();
|
||||||
|
|
||||||
|
this._resizedSinceLastRender = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._scrollRegionBottom = scrollBottom;
|
this._scrollRegionBottom = scrollBottom;
|
||||||
|
|
||||||
System.Console.Write(AnsiEscapes.SetScrollRegion(scrollBottom));
|
System.Console.Write(AnsiEscapes.SetScrollRegion(scrollBottom));
|
||||||
|
|
||||||
// Render text scroll panel in the scroll area (all items except the last)
|
// Render text scroll panel in the scroll area
|
||||||
IReadOnlyList<string> scrollItems = state.ScrollAreaContentItems.Count > 1
|
|
||||||
? state.ScrollAreaContentItems.Take(state.ScrollAreaContentItems.Count - 1).ToList()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
this._textScrollPanel.Props = new TextScrollPanelProps
|
this._textScrollPanel.Props = new TextScrollPanelProps
|
||||||
{
|
{
|
||||||
X = 1,
|
X = 1,
|
||||||
Y = 1,
|
Y = 1,
|
||||||
Width = state.ConsoleWidth,
|
Width = state.ConsoleWidth,
|
||||||
Height = scrollBottom,
|
Height = scrollBottom,
|
||||||
Items = scrollItems,
|
Items = state.ScrollAreaContentItems,
|
||||||
};
|
};
|
||||||
this._textScrollPanel.Render();
|
this._textScrollPanel.Render();
|
||||||
|
|
||||||
// Render the text panel for the last (dynamic) item just below the scroll region
|
// Render queued input items between scroll area and agent status
|
||||||
this._textPanel.Props = new TextPanelProps
|
int queuedPanelY = scrollBottom + 1;
|
||||||
{
|
|
||||||
X = 1,
|
|
||||||
Y = scrollBottom + 1,
|
|
||||||
Width = state.ConsoleWidth,
|
|
||||||
Height = textPanelHeight,
|
|
||||||
Items = lastItems,
|
|
||||||
};
|
|
||||||
this._textPanel.Render();
|
|
||||||
|
|
||||||
// Render queued input items between text panel and agent status
|
|
||||||
int queuedPanelY = scrollBottom + textPanelHeight + 1;
|
|
||||||
this._queuedPanel.Props = new TextPanelProps
|
this._queuedPanel.Props = new TextPanelProps
|
||||||
{
|
{
|
||||||
X = 1,
|
X = 1,
|
||||||
|
|||||||
+7
-5
@@ -88,16 +88,17 @@ public sealed class PlanningOutputObserver : ConsoleObserver
|
|||||||
{
|
{
|
||||||
planningResponse = JsonSerializer.Deserialize<PlanningResponse>(collectedText);
|
planningResponse = JsonSerializer.Deserialize<PlanningResponse>(collectedText);
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
await ux.WriteInfoLineAsync($"❌ Failed to parse planning response: {ex.Message}", ConsoleColor.Red);
|
// JSON parsing failed — fall back to rendering as regular text output.
|
||||||
await ux.WriteInfoLineAsync($"(raw response) {collectedText}", ConsoleColor.DarkYellow);
|
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planningResponse is null)
|
if (planningResponse is null)
|
||||||
{
|
{
|
||||||
await ux.WriteInfoLineAsync("(no structured response from agent)", ConsoleColor.DarkYellow);
|
// Null result — fall back to rendering as regular text output.
|
||||||
|
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +119,8 @@ public sealed class PlanningOutputObserver : ConsoleObserver
|
|||||||
return new List<FollowUpAction> { this.BuildApprovalAction(question, session) };
|
return new List<FollowUpAction> { this.BuildApprovalAction(question, session) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await ux.WriteInfoLineAsync($"(unexpected response type: {planningResponse.Type})", ConsoleColor.DarkYellow);
|
// Unexpected type — fall back to rendering as regular text output.
|
||||||
|
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -5,17 +5,17 @@ using Microsoft.Extensions.AI;
|
|||||||
namespace Harness.Shared.Console.ToolFormatters;
|
namespace Harness.Shared.Console.ToolFormatters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Formats <c>AgentMode_*</c> tool calls, showing the target mode for Set operations.
|
/// Formats <c>mode_*</c> tool calls, showing the target mode for Set operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ModeToolFormatter : ToolCallFormatter
|
public sealed class ModeToolFormatter : ToolCallFormatter
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("AgentMode_", StringComparison.Ordinal);
|
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("mode_", StringComparison.Ordinal);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
||||||
{
|
{
|
||||||
"AgentMode_Set" => FormatStringArg(call, "mode"),
|
"mode_set" => FormatStringArg(call, "mode"),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -7,20 +7,20 @@ using Microsoft.Extensions.AI;
|
|||||||
namespace Harness.Shared.Console.ToolFormatters;
|
namespace Harness.Shared.Console.ToolFormatters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Formats <c>TodoList_*</c> tool calls with tree-view output for added items
|
/// Formats <c>todos_*</c> tool calls with tree-view output for added items
|
||||||
/// and structured output for complete/remove operations.
|
/// and structured output for complete/remove operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TodoToolFormatter : ToolCallFormatter
|
public sealed class TodoToolFormatter : ToolCallFormatter
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("TodoList_", StringComparison.Ordinal);
|
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("todos_", StringComparison.Ordinal);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
||||||
{
|
{
|
||||||
"TodoList_Add" => FormatAddTodos(call),
|
"todos_add" => FormatAddTodos(call),
|
||||||
"TodoList_Complete" => FormatCompleteTodos(call),
|
"todos_complete" => FormatCompleteTodos(call),
|
||||||
"TodoList_Remove" => FormatIdList(call, "ids", "Remove"),
|
"todos_remove" => FormatIdList(call, "ids", "Remove"),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+88
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
|
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
using Harness.Shared.Console.Observers;
|
using Harness.Shared.Console.Observers;
|
||||||
using Microsoft.Agents.AI;
|
using Microsoft.Agents.AI;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
@@ -53,9 +54,94 @@ public sealed class OpenAIResponsesErrorObserver : ConsoleObserver
|
|||||||
|
|
||||||
case StreamingResponseIncompleteUpdate incompleteUpdate:
|
case StreamingResponseIncompleteUpdate incompleteUpdate:
|
||||||
string? reason = incompleteUpdate.Response?.IncompleteStatusDetails?.Reason?.ToString();
|
string? reason = incompleteUpdate.Response?.IncompleteStatusDetails?.Reason?.ToString();
|
||||||
string incompleteText = $"⚠️ Response incomplete: {reason ?? "unknown reason"}";
|
if (string.Equals(reason, "content_filter", StringComparison.OrdinalIgnoreCase))
|
||||||
await ux.WriteInfoLineAsync(incompleteText, ConsoleColor.Yellow);
|
{
|
||||||
|
string detail = GetContentFilterDetails(incompleteUpdate);
|
||||||
|
const string Message = "🛡️ The service's built-in content filter guardrails were triggered and the response was cut short.";
|
||||||
|
await ux.WriteInfoLineAsync(
|
||||||
|
string.IsNullOrEmpty(detail) ? Message : $"{Message}\n{detail}",
|
||||||
|
ConsoleColor.Yellow);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string incompleteText = $"⚠️ Response incomplete: {reason ?? "unknown reason"}";
|
||||||
|
await ux.WriteInfoLineAsync(incompleteText, ConsoleColor.Yellow);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts content filter details from the serialized response JSON and returns
|
||||||
|
/// a formatted string showing which specific categories were triggered.
|
||||||
|
/// Returns <see cref="string.Empty"/> if details cannot be extracted.
|
||||||
|
/// </summary>
|
||||||
|
private static string GetContentFilterDetails(StreamingResponseIncompleteUpdate incompleteUpdate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = System.ClientModel.Primitives.ModelReaderWriter.Write(incompleteUpdate);
|
||||||
|
using var doc = JsonDocument.Parse(data.ToString());
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// Navigate into the nested response object if present.
|
||||||
|
JsonElement responseElement = root.TryGetProperty("response", out var resp) ? resp : root;
|
||||||
|
|
||||||
|
if (!responseElement.TryGetProperty("content_filters", out var filtersArray)
|
||||||
|
|| filtersArray.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var filter in filtersArray.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!filter.TryGetProperty("content_filter_results", out var results)
|
||||||
|
|| results.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect category data for aligned output.
|
||||||
|
var categories = new List<(string Name, bool Filtered, string? Severity)>();
|
||||||
|
foreach (var category in results.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (category.Value.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filtered = category.Value.TryGetProperty("filtered", out var f) && f.GetBoolean();
|
||||||
|
string? severity = category.Value.TryGetProperty("severity", out var s) ? s.GetString() : null;
|
||||||
|
categories.Add((category.Name, filtered, severity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build all category lines into a single string.
|
||||||
|
int maxNameLen = categories.Count > 0 ? categories.Max(c => c.Name.Length) : 0;
|
||||||
|
var lines = new List<string>();
|
||||||
|
|
||||||
|
foreach (var (name, filtered, severity) in categories)
|
||||||
|
{
|
||||||
|
string paddedName = name.PadRight(maxNameLen);
|
||||||
|
string icon = filtered ? "❌" : "✅";
|
||||||
|
string statusText = filtered ? "Filtered " : "Not Filtered";
|
||||||
|
string severityText = severity is not null ? $" Severity: {severity}" : "";
|
||||||
|
|
||||||
|
lines.Add($" {icon} {paddedName} {statusText}{severityText}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.Count > 0)
|
||||||
|
{
|
||||||
|
return string.Join("\n", lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Parsing not critical — skip silently if it fails.
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||||
|
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<NoWarn>$(NoWarn);MAAI001;MEAI001;MCPEXP001</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.AI.OpenAI" />
|
||||||
|
<PackageReference Include="Azure.Identity" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
// This sample demonstrates the Microsoft Agent Framework's MCP long-running task support.
|
||||||
|
//
|
||||||
|
// A small MCP server (hosted in this same executable when launched with "--server") exposes
|
||||||
|
// a single task-supporting tool "AnalyzeDataset" that simulates ~15 seconds of work. The
|
||||||
|
// client (default mode) connects to it over stdio via Microsoft.Agents.AI.Mcp's
|
||||||
|
// McpClientTaskExtensions.ListAgentToolsWithTaskSupportAsync, hands the wrapped tools to a
|
||||||
|
// ChatClientAgent, and exercises both invocation styles:
|
||||||
|
// * RunAsync — blocks until the agent's final response is ready.
|
||||||
|
// * RunStreamingAsync — yields response updates as the model produces them; the model
|
||||||
|
// still waits for the tool's terminal result before it can begin
|
||||||
|
// producing the final answer, so the perceived "pause" reflects
|
||||||
|
// tool execution time, not stream-channel latency.
|
||||||
|
//
|
||||||
|
// In both cases the wrapper transparently:
|
||||||
|
// 1. Calls tools/call with task augmentation (CallToolAsTaskAsync)
|
||||||
|
// 2. Polls tasks/get until terminal (PollTaskUntilCompleteAsync)
|
||||||
|
// 3. Fetches tasks/result and returns the final result to the function-calling loop
|
||||||
|
//
|
||||||
|
// No application-level loop or continuation tokens are required in either mode.
|
||||||
|
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Azure.AI.OpenAI;
|
||||||
|
using Azure.Identity;
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using Microsoft.Agents.AI.Mcp;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ModelContextProtocol;
|
||||||
|
using ModelContextProtocol.Client;
|
||||||
|
using ModelContextProtocol.Protocol;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using OpenAI.Chat;
|
||||||
|
|
||||||
|
if (args.Length > 0 && args[0] == "--server")
|
||||||
|
{
|
||||||
|
await RunMcpServerAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
// Launch this same assembly as a stdio MCP server in a child process.
|
||||||
|
var thisAssemblyPath = typeof(Program).Assembly.Location;
|
||||||
|
await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new()
|
||||||
|
{
|
||||||
|
Name = "DatasetAnalyzer",
|
||||||
|
Command = "dotnet",
|
||||||
|
Arguments = [thisAssemblyPath, "--server"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wrap each MCP tool with task-aware behavior. The wrapper inspects the server's
|
||||||
|
// execution.taskSupport on each tool and, when it is Required, drives the task lifecycle
|
||||||
|
// transparently within the agent's tool loop. Tools that don't require task semantics are
|
||||||
|
// returned as-is and invoked inline.
|
||||||
|
var taskOptions = new McpTaskOptions
|
||||||
|
{
|
||||||
|
DefaultTimeToLive = TimeSpan.FromMinutes(5),
|
||||||
|
};
|
||||||
|
var mcpTools = await mcpClient.ListAgentToolsWithTaskSupportAsync(taskOptions);
|
||||||
|
|
||||||
|
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||||
|
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||||
|
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||||
|
AIAgent agent = new AzureOpenAIClient(
|
||||||
|
new Uri(endpoint),
|
||||||
|
new DefaultAzureCredential())
|
||||||
|
.GetChatClient(deploymentName)
|
||||||
|
.AsAIAgent(
|
||||||
|
instructions: "You answer data-analysis questions by invoking the available tools. Always invoke a tool when one matches the request.",
|
||||||
|
tools: [.. mcpTools.Cast<AITool>()]);
|
||||||
|
|
||||||
|
const string Prompt = "Analyze the dataset named 'sales-2025-q1' and summarize the findings.";
|
||||||
|
|
||||||
|
Console.WriteLine("=== Transparent long-running MCP task (RunAsync) ===");
|
||||||
|
Console.WriteLine("Asking the agent to analyze a dataset; the tool takes ~15s to complete.");
|
||||||
|
Console.WriteLine("RunAsync blocks while the wrapper polls the task to completion.");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
var response = await agent.RunAsync(Prompt);
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
Console.WriteLine($"Agent response (after {stopwatch.Elapsed.TotalSeconds:F1}s):");
|
||||||
|
Console.WriteLine(response.Text);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("=== Transparent long-running MCP task (RunStreamingAsync) ===");
|
||||||
|
Console.WriteLine("Same request via the streaming API. Updates only begin to arrive after the");
|
||||||
|
Console.WriteLine("tool's task reaches the Completed state, since the model needs the tool result");
|
||||||
|
Console.WriteLine("before it can produce its final answer.");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
stopwatch.Restart();
|
||||||
|
await foreach (var update in agent.RunStreamingAsync(Prompt))
|
||||||
|
{
|
||||||
|
Console.Write(update.Text);
|
||||||
|
}
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"(Streaming completed after {stopwatch.Elapsed.TotalSeconds:F1}s.)");
|
||||||
|
|
||||||
|
// --- Server mode (launched as a child process via --server) ---------------------------------
|
||||||
|
static async Task RunMcpServerAsync()
|
||||||
|
{
|
||||||
|
var builder = Host.CreateApplicationBuilder();
|
||||||
|
|
||||||
|
// Critical for stdio transport: any provider that writes to stdout will corrupt the
|
||||||
|
// JSON-RPC channel. Clear all providers; the MCP SDK routes its own diagnostics
|
||||||
|
// appropriately.
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
|
||||||
|
|
||||||
|
builder.Services.AddMcpServer(o =>
|
||||||
|
{
|
||||||
|
o.TaskStore = new InMemoryMcpTaskStore();
|
||||||
|
o.ServerInfo = new Implementation { Name = "DatasetAnalyzer", Version = "1.0.0" };
|
||||||
|
})
|
||||||
|
.WithStdioServerTransport()
|
||||||
|
.WithTools<DatasetAnalysisTools>();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1812 // Discovered by MCP SDK via [McpServerToolType] attribute
|
||||||
|
[McpServerToolType]
|
||||||
|
internal sealed class DatasetAnalysisTools
|
||||||
|
#pragma warning restore CA1812
|
||||||
|
{
|
||||||
|
[McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Required)]
|
||||||
|
[Description("Analyze a tabular dataset and return summary statistics. This tool simulates a long-running analytic job (~15 seconds).")]
|
||||||
|
public static async Task<string> AnalyzeDatasetAsync(
|
||||||
|
[Description("The dataset identifier, e.g. 'sales-2025-q1'.")] string datasetName,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return $"Findings for '{datasetName}': 12,403 rows; avg revenue $48,712; 3 anomalies detected in week 7; outliers concentrated in EMEA region.";
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
# Agent with MCP long-running task (transparent polling)
|
||||||
|
|
||||||
|
This sample demonstrates Microsoft Agent Framework's MCP long-running task support: an agent invokes an MCP tool whose execution takes too long for a single request/response cycle, and the framework polls it to completion behind the function-calling loop. From the agent's perspective the tool simply returns its result.
|
||||||
|
|
||||||
|
## What this sample shows
|
||||||
|
|
||||||
|
- Using `McpClient.ListAgentToolsWithTaskSupportAsync(...)` (in `Microsoft.Agents.AI.Mcp`) to wrap MCP tools with task-aware behavior.
|
||||||
|
- Configuring `McpTaskOptions.DefaultTimeToLive` to bound the server-side task.
|
||||||
|
- Hosting a small MCP server (in this same executable, launched with `--server`) that advertises `execution.taskSupport=required` on a tool that sleeps for ~15 seconds.
|
||||||
|
- No application-level polling, continuation tokens, or `AllowBackgroundResponses` flag are required.
|
||||||
|
|
||||||
|
The decorator drives the lifecycle internally:
|
||||||
|
|
||||||
|
1. `tools/call` augmented with task metadata (`CallToolAsTaskAsync`)
|
||||||
|
2. `tasks/get` polled until terminal (`PollTaskUntilCompleteAsync`)
|
||||||
|
3. `tasks/result` retrieved (`GetTaskResultAsync`) and returned to the function-calling loop
|
||||||
|
|
||||||
|
The sample exercises both invocation styles against the same wrapper:
|
||||||
|
|
||||||
|
- `agent.RunAsync(...)` blocks until the tool completes (~15 seconds in this sample) and returns the final response.
|
||||||
|
- `agent.RunStreamingAsync(...)` returns immediately and yields `AgentResponseUpdate` chunks as the model emits them; in this scenario the model only begins streaming its answer once the wrapped tool's task reaches the `Completed` state, so the perceived "pause" before tokens arrive reflects tool execution time, not stream-channel latency.
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
|
||||||
|
- .NET 10 SDK or later
|
||||||
|
- Azure OpenAI service endpoint and a chat-completions deployment
|
||||||
|
- Azure CLI installed and authenticated (`az login`)
|
||||||
|
|
||||||
|
Set the following environment variables:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$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
|
||||||
|
```
|
||||||
|
|
||||||
|
# Running
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd Agent_MCP_LongRunningTask_Client
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output similar to:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== Transparent long-running MCP task (RunAsync) ===
|
||||||
|
Asking the agent to analyze a dataset; the tool takes ~15s to complete.
|
||||||
|
RunAsync blocks while the wrapper polls the task to completion.
|
||||||
|
|
||||||
|
Agent response (after 15.4s):
|
||||||
|
The 'sales-2025-q1' dataset contains 12,403 rows ...
|
||||||
|
|
||||||
|
=== Transparent long-running MCP task (RunStreamingAsync) ===
|
||||||
|
Same request via the streaming API. Updates only begin to arrive after the
|
||||||
|
tool's task reaches the Completed state, since the model needs the tool result
|
||||||
|
before it can produce its final answer.
|
||||||
|
|
||||||
|
The 'sales-2025-q1' dataset contains 12,403 rows ...
|
||||||
|
(Streaming completed after 15.7s.)
|
||||||
|
```
|
||||||
@@ -22,6 +22,7 @@ Before you begin, ensure you have the following prerequisites:
|
|||||||
|[Agent with MCP server tools](./Agent_MCP_Server/)|This sample demonstrates how to use MCP server tools with a simple agent|
|
|[Agent with MCP server tools](./Agent_MCP_Server/)|This sample demonstrates how to use MCP server tools with a simple agent|
|
||||||
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
|
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
|
||||||
|[Responses Agent with Hosted MCP tool](./ResponseAgent_Hosted_MCP/)|This sample demonstrates how to use the Hosted MCP tool with the Responses Service, where the service invokes any MCP tools directly|
|
|[Responses Agent with Hosted MCP tool](./ResponseAgent_Hosted_MCP/)|This sample demonstrates how to use the Hosted MCP tool with the Responses Service, where the service invokes any MCP tools directly|
|
||||||
|
|[Agent with long-running MCP task (transparent polling)](./Agent_MCP_LongRunningTask_Client/)|This sample demonstrates how an agent transparently drives a long-running MCP task (SEP-2663) to completion. The wrapper polls the task internally on both `RunAsync` and `RunStreamingAsync` invocations.|
|
||||||
|
|
||||||
## Running the samples from the console
|
## Running the samples from the console
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
|
||||||
<NoWarn>$(NoWarn);CA1812</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<InjectSharedThrow>true</InjectSharedThrow>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
|
||||||
|
|
||||||
namespace Demo.DeclarativeEject;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HOW TO: Convert a workflow from a declartive (yaml based) definition to code.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <b>Usage</b>
|
|
||||||
/// Provide the path to the workflow definition file as the first argument.
|
|
||||||
/// All other arguments are intepreted as a queue of inputs.
|
|
||||||
/// When no input is queued, interactive input is requested from the console.
|
|
||||||
/// </remarks>
|
|
||||||
internal sealed class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
Program program = new(args);
|
|
||||||
program.Execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Execute()
|
|
||||||
{
|
|
||||||
// Read and parse the declarative workflow.
|
|
||||||
Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
|
|
||||||
|
|
||||||
Stopwatch timer = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
// Use DeclarativeWorkflowBuilder to generate code based on a YAML file.
|
|
||||||
string code =
|
|
||||||
DeclarativeWorkflowBuilder.Eject(
|
|
||||||
this.WorkflowFile,
|
|
||||||
DeclarativeWorkflowLanguage.CSharp,
|
|
||||||
workflowNamespace: "Demo.DeclarativeCode",
|
|
||||||
workflowPrefix: "Sample");
|
|
||||||
|
|
||||||
Notify($"\nWORKFLOW: Defined {timer.Elapsed}\n");
|
|
||||||
|
|
||||||
Console.WriteLine(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string DefaultWorkflow = "Marketing.yaml";
|
|
||||||
|
|
||||||
private string WorkflowFile { get; }
|
|
||||||
|
|
||||||
private Program(string[] args)
|
|
||||||
{
|
|
||||||
this.WorkflowFile = ParseWorkflowFile(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ParseWorkflowFile(string[] args)
|
|
||||||
{
|
|
||||||
string workflowFile = args.FirstOrDefault() ?? DefaultWorkflow;
|
|
||||||
|
|
||||||
if (!File.Exists(workflowFile) && !Path.IsPathFullyQualified(workflowFile))
|
|
||||||
{
|
|
||||||
string? repoFolder = GetRepoFolder();
|
|
||||||
if (repoFolder is not null)
|
|
||||||
{
|
|
||||||
workflowFile = Path.Combine(repoFolder, "declarative-agents", "workflow-samples", workflowFile);
|
|
||||||
workflowFile = Path.ChangeExtension(workflowFile, ".yaml");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(workflowFile))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Unable to locate workflow: {Path.GetFullPath(workflowFile)}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflowFile;
|
|
||||||
|
|
||||||
static string? GetRepoFolder()
|
|
||||||
{
|
|
||||||
DirectoryInfo? current = new(Directory.GetCurrentDirectory());
|
|
||||||
|
|
||||||
while (current is not null)
|
|
||||||
{
|
|
||||||
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
|
|
||||||
{
|
|
||||||
return current.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = current.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Notify(string message)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine(message);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"Marketing": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "\"Marketing.yaml\""
|
|
||||||
},
|
|
||||||
"MathChat": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "\"MathChat.yaml\""
|
|
||||||
},
|
|
||||||
"Question": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "\"Question.yaml\""
|
|
||||||
},
|
|
||||||
"Research": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "\"DeepResearch.yaml\""
|
|
||||||
},
|
|
||||||
"ResponseObject": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "\"ResponseObject.yaml\""
|
|
||||||
},
|
|
||||||
"UserInput": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "\"UserInput.yaml\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,12 +50,16 @@ internal static partial class WorkflowHelper
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executor that starts the concurrent processing by sending messages to the agents.
|
/// Executor that starts the concurrent processing by sending messages to the agents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
|
[SendsMessage(typeof(List<ChatMessage>))]
|
||||||
|
[SendsMessage(typeof(TurnToken))]
|
||||||
|
private sealed partial class ConcurrentStartExecutor()
|
||||||
|
: Executor("ConcurrentStartExecutor", declareCrossRunShareable: true), IResettableExecutor
|
||||||
{
|
{
|
||||||
[MessageHandler]
|
[MessageHandler]
|
||||||
internal ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
internal ValueTask RouteMessages(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
|
List<ChatMessage> payload = messages as List<ChatMessage> ?? messages.ToList();
|
||||||
|
return context.SendMessageAsync(payload, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessageHandler]
|
[MessageHandler]
|
||||||
@@ -63,13 +67,16 @@ internal static partial class WorkflowHelper
|
|||||||
{
|
{
|
||||||
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
|
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValueTask ResetAsync() => default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executor that aggregates the results from the concurrent agents.
|
/// Executor that aggregates the results from the concurrent agents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[YieldsOutput(typeof(List<ChatMessage>))]
|
[YieldsOutput(typeof(string))]
|
||||||
private sealed partial class ConcurrentAggregationExecutor() : Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
|
private sealed partial class ConcurrentAggregationExecutor() :
|
||||||
|
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
|
||||||
{
|
{
|
||||||
private readonly List<ChatMessage> _messages = [];
|
private readonly List<ChatMessage> _messages = [];
|
||||||
|
|
||||||
@@ -90,5 +97,11 @@ internal static partial class WorkflowHelper
|
|||||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValueTask ResetAsync()
|
||||||
|
{
|
||||||
|
this._messages.Clear();
|
||||||
|
return default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||||
|
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<NoWarn>$(NoWarn);MAAIW001;OPENAI001</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
|
<PackageReference Include="Azure.Identity" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
// This sample ports the Python Magentic orchestration sample to .NET.
|
||||||
|
// A Magentic workflow coordinates a researcher and a coder, streams orchestration
|
||||||
|
// events as the plan evolves, and prints the final conversation transcript.
|
||||||
|
|
||||||
|
using Azure.AI.Projects;
|
||||||
|
using Azure.Identity;
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using Microsoft.Agents.AI.Workflows;
|
||||||
|
using Microsoft.Agents.AI.Workflows.Specialized.Magentic;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
namespace WorkflowMagenticOrchestrationSample;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Demonstrates Magentic orchestration with a researcher, a coder, and an LLM manager.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Pre-requisites:
|
||||||
|
/// - An Azure AI Foundry project endpoint and model deployment must be configured.
|
||||||
|
/// - Run <c>az login</c> before executing the sample.
|
||||||
|
/// </remarks>
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
private const string TaskPrompt =
|
||||||
|
"I am preparing a report on the energy efficiency of different machine learning model architectures. " +
|
||||||
|
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 " +
|
||||||
|
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). " +
|
||||||
|
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 " +
|
||||||
|
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model " +
|
||||||
|
"per task type (image classification, text classification, and text generation).";
|
||||||
|
|
||||||
|
private static async Task Main()
|
||||||
|
{
|
||||||
|
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||||
|
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||||
|
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||||
|
|
||||||
|
// 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 projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||||
|
|
||||||
|
AIAgent researcherAgent = projectClient.AsAIAgent(
|
||||||
|
deploymentName,
|
||||||
|
name: "ResearcherAgent",
|
||||||
|
description: "Specialist in research and information gathering.",
|
||||||
|
instructions: "You are a researcher. Find relevant information without doing additional computation or quantitative analysis.");
|
||||||
|
|
||||||
|
AIAgent coderAgent = projectClient.AsAIAgent(
|
||||||
|
deploymentName,
|
||||||
|
name: "CoderAgent",
|
||||||
|
description: "A helpful assistant that writes and executes code to analyze data.",
|
||||||
|
instructions: "You solve quantitative questions by writing and running code. Show the analysis and the computation process clearly.",
|
||||||
|
tools: [new HostedCodeInterpreterTool()]);
|
||||||
|
|
||||||
|
AIAgent managerAgent = projectClient.AsAIAgent(
|
||||||
|
deploymentName,
|
||||||
|
name: "MagenticManager",
|
||||||
|
description: "Orchestrator that coordinates the research and coding workflow.",
|
||||||
|
instructions: "You coordinate the team to complete complex tasks efficiently.");
|
||||||
|
|
||||||
|
Workflow workflow = new MagenticWorkflowBuilder(managerAgent)
|
||||||
|
.AddParticipants([researcherAgent, coderAgent])
|
||||||
|
.WithName("Magentic Orchestration Workflow")
|
||||||
|
.WithDescription("Coordinates a researcher and coder to solve a complex analytical task.")
|
||||||
|
.RequirePlanSignoff(false)
|
||||||
|
.WithMaxRounds(10)
|
||||||
|
.WithMaxStalls(3)
|
||||||
|
.WithMaxResets(2)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Console.WriteLine("Building Magentic workflow...");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Task: {TaskPrompt}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Starting workflow execution...");
|
||||||
|
|
||||||
|
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(
|
||||||
|
workflow,
|
||||||
|
new List<ChatMessage> { new(ChatRole.User, TaskPrompt) });
|
||||||
|
|
||||||
|
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||||
|
|
||||||
|
string? lastResponseId = null;
|
||||||
|
WorkflowOutputEvent? finalOutput = null;
|
||||||
|
|
||||||
|
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
|
||||||
|
{
|
||||||
|
switch (workflowEvent)
|
||||||
|
{
|
||||||
|
case AgentResponseUpdateEvent updateEvent:
|
||||||
|
WriteStreamingUpdate(updateEvent, ref lastResponseId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MagenticPlanCreatedEvent planCreated:
|
||||||
|
WriteMagenticMessage("Initial Plan", planCreated.FullTaskLedger.Text);
|
||||||
|
PauseIfInteractive();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MagenticReplannedEvent replanned:
|
||||||
|
WriteMagenticMessage("Replanned", replanned.FullTaskLedger.Text);
|
||||||
|
PauseIfInteractive();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MagenticProgressLedgerUpdatedEvent progressUpdated:
|
||||||
|
WriteMagenticMessage("Progress Ledger", FormatProgressLedger(progressUpdated.ProgressLedger));
|
||||||
|
PauseIfInteractive();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WorkflowOutputEvent outputEvent when outputEvent.Is<List<ChatMessage>>():
|
||||||
|
finalOutput = outputEvent;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WorkflowErrorEvent workflowError:
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.Error.WriteLine(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred.");
|
||||||
|
Console.ResetColor();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ExecutorFailedEvent executorFailed:
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.Error.WriteLine($"Executor '{executorFailed.ExecutorId}' failed with {(executorFailed.Data is null ? "unknown error" : $"exception {executorFailed.Data}")}.");
|
||||||
|
Console.ResetColor();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalOutput?.As<List<ChatMessage>>() is { } transcript)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(new string('=', 80));
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Final Conversation Transcript:");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
foreach (ChatMessage message in transcript)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{message.AuthorName ?? message.Role.ToString()}: {message.Text}");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteStreamingUpdate(AgentResponseUpdateEvent updateEvent, ref string? lastResponseId)
|
||||||
|
{
|
||||||
|
string responseId = updateEvent.Update.ResponseId ?? updateEvent.Update.MessageId ?? updateEvent.ExecutorId;
|
||||||
|
if (!string.Equals(responseId, lastResponseId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (lastResponseId is not null)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write($"- {updateEvent.ExecutorId}: ");
|
||||||
|
lastResponseId = responseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(updateEvent.Update.Text))
|
||||||
|
{
|
||||||
|
Console.Write(updateEvent.Update.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteMagenticMessage(string title, string? content)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"[Magentic {title}]");
|
||||||
|
Console.WriteLine(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatProgressLedger(MagenticProgressLedger ledger) =>
|
||||||
|
string.Join(Environment.NewLine,
|
||||||
|
$"Request satisfied: {ledger.IsRequestSatisfied}",
|
||||||
|
$"In loop: {ledger.IsInLoop}",
|
||||||
|
$"Making progress: {ledger.IsProgressBeingMade}",
|
||||||
|
$"Next speaker: {ledger.NextSpeaker}",
|
||||||
|
$"Instruction: {ledger.InstructionOrQuestion}");
|
||||||
|
|
||||||
|
private static void PauseIfInteractive()
|
||||||
|
{
|
||||||
|
if (Console.IsInputRedirected || Console.IsOutputRedirected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write("Press Enter to continue...");
|
||||||
|
Console.ReadLine();
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Magentic Orchestration Sample
|
||||||
|
|
||||||
|
This sample showcases the Magentic Orchestration Pattern in .NET, setting up a team with three roles:
|
||||||
|
|
||||||
|
- **ResearcherAgent** gathers factual background information.
|
||||||
|
- **CoderAgent** uses `HostedCodeInterpreterTool` for quantitative analysis.
|
||||||
|
- **MagenticManager** plans the work, tracks progress, and decides who should act next.
|
||||||
|
|
||||||
|
## What This Sample Demonstrates
|
||||||
|
|
||||||
|
- Building a Magentic workflow with `MagenticWorkflowBuilder`
|
||||||
|
- Combining standard responses-based agents with a code interpreter-enabled participant
|
||||||
|
- Streaming orchestration events such as the initial plan, replans, and progress-ledger updates
|
||||||
|
- Printing the final multi-agent conversation transcript
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- `AZURE_AI_PROJECT_ENDPOINT` set to your Azure AI Foundry project endpoint
|
||||||
|
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` set to your model deployment name (defaults to `gpt-5.4-mini`)
|
||||||
|
- `az login` completed before running the sample
|
||||||
|
|
||||||
|
## Running the Sample
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
The sample prints:
|
||||||
|
|
||||||
|
1. The original task prompt
|
||||||
|
2. Streamed updates from the participating agents
|
||||||
|
3. Magentic plan and progress-ledger events as the workflow coordinates the team
|
||||||
|
4. The final conversation transcript returned by the workflow
|
||||||
|
|
||||||
|
## Related Samples
|
||||||
|
|
||||||
|
- [Handoff Orchestration](../Handoff) - another multi-agent orchestration pattern in .NET workflows
|
||||||
|
- [Python Magentic workflow sample](../../../../../python/samples/03-workflows/orchestrations/magentic.py) - the source scenario that this sample ports
|
||||||
@@ -62,3 +62,4 @@ Once completed, please proceed to the other samples listed below.
|
|||||||
| Sample | Concepts |
|
| Sample | Concepts |
|
||||||
|--------|----------|
|
|--------|----------|
|
||||||
| [Handoff Orchestration](./Orchestration/Handoff) | Introduces the Handoff Orchestration pattern |
|
| [Handoff Orchestration](./Orchestration/Handoff) | Introduces the Handoff Orchestration pattern |
|
||||||
|
| [Magentic Orchestration](./Orchestration/Magentic) | Coordinates multiple agents with a Magentic manager, streamed plan events, and a final transcript |
|
||||||
|
|||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>",
|
|
||||||
"REDIS_CONNECTION_STRING": "localhost:6379",
|
|
||||||
"REDIS_STREAM_TTL_MINUTES": "10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"IsEncrypted": false,
|
|
||||||
"Values": {
|
|
||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
|
||||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
||||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
|
||||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
|
||||||
"AZURE_OPENAI_DEPLOYMENT_NAME": "<AZURE_OPENAI_DEPLOYMENT_NAME>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||||
|
ASPNETCORE_URLS=http://+:8088
|
||||||
|
ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||||
|
AGENT_NAME=hosted-agent-skills
|
||||||
|
SKILL_NAMES=support-style,escalation-policy
|
||||||
|
# Set to true to provision sample skills to Foundry on startup (first-run convenience).
|
||||||
|
# In production, skills are provisioned externally — leave this unset or false.
|
||||||
|
PROVISION_SAMPLE_SKILLS=true
|
||||||
|
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||||
|
# When running outside the Foundry platform the platform-injected isolation keys are absent.
|
||||||
|
# These two variables provide fallback values for local Docker debugging only.
|
||||||
|
HOSTED_USER_ISOLATION_KEY=local-dev-user
|
||||||
|
HOSTED_CHAT_ISOLATION_KEY=local-dev-chat
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Dockerfile for end-users consuming the Agent Framework via NuGet packages.
|
||||||
|
#
|
||||||
|
# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container,
|
||||||
|
# which only succeeds when the project references its dependencies via PackageReference (see the
|
||||||
|
# commented-out section in HostedAgentSkills.csproj). Contributors building from the
|
||||||
|
# agent-framework repository source must use Dockerfile.contributor instead because
|
||||||
|
# ProjectReference dependencies live outside this folder and cannot be restored from inside
|
||||||
|
# this build context.
|
||||||
|
#
|
||||||
|
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet restore
|
||||||
|
RUN dotnet publish -c Release -o /app/publish
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
EXPOSE 8088
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8088
|
||||||
|
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Dockerfile for contributors building from the agent-framework repository source.
|
||||||
|
#
|
||||||
|
# This project uses ProjectReference to the local Microsoft.Agents.AI source,
|
||||||
|
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||||
|
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||||
|
# the output into the container:
|
||||||
|
#
|
||||||
|
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||||
|
# docker build -f Dockerfile.contributor -t hosted-agent-skills .
|
||||||
|
# docker run --rm -p 8088:8088 \
|
||||||
|
# -e AGENT_NAME=hosted-agent-skills \
|
||||||
|
# -e HOSTED_USER_ISOLATION_KEY=alice \
|
||||||
|
# -e HOSTED_CHAT_ISOLATION_KEY=alice-chat-1 \
|
||||||
|
# --env-file .env hosted-agent-skills
|
||||||
|
#
|
||||||
|
# For end-users consuming the NuGet package (not ProjectReference), use the standard
|
||||||
|
# Dockerfile which performs a full dotnet restore + publish inside the container.
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY out/ .
|
||||||
|
EXPOSE 8088
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8088
|
||||||
|
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||||
|
<RootNamespace>HostedAgentSkills</RootNamespace>
|
||||||
|
<AssemblyName>HostedAgentSkills</AssemblyName>
|
||||||
|
<NoWarn>$(NoWarn);MEAI001;OPENAI001;AAIP001</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
|
<PackageReference Include="Azure.Identity" />
|
||||||
|
<PackageReference Include="DotNetEnv" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
||||||
|
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Agents.AI" Version="1.6.1" />
|
||||||
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Include the skills/ directory in the publish output so the sample can provision them -->
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
// Hosted-AgentSkills
|
||||||
|
//
|
||||||
|
// Demonstrates how to host an agent that loads its behavioral guidelines from Foundry Skills at
|
||||||
|
// startup. Skills are authored as SKILL.md files, uploaded to Foundry via the Skills REST API,
|
||||||
|
// and downloaded by the agent on boot so guideline updates ship without code changes.
|
||||||
|
//
|
||||||
|
// The agent uses AgentSkillsProvider from the Agent Framework which implements the progressive
|
||||||
|
// disclosure pattern from the Agent Skills specification (https://agentskills.io/):
|
||||||
|
// 1. Advertise — skill names and descriptions are injected into the system prompt.
|
||||||
|
// 2. Load — the model calls load_skill to retrieve the full SKILL.md body on demand.
|
||||||
|
//
|
||||||
|
// IMPORTANT: In production, skill provisioning (uploading SKILL.md files to Foundry) is an
|
||||||
|
// external concern — it is NOT the hosted agent's responsibility. The provisioning helper below
|
||||||
|
// is included for sample convenience only, so the sample is self-contained and runnable without
|
||||||
|
// a separate setup step. A real deployment pipeline would provision skills separately (e.g., via
|
||||||
|
// a CI/CD step, a CLI script, or a management portal).
|
||||||
|
|
||||||
|
#pragma warning disable AAIP001 // ProjectAgentSkills is experimental
|
||||||
|
|
||||||
|
using System.ClientModel;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using Azure.AI.Projects;
|
||||||
|
using Azure.AI.Projects.Agents;
|
||||||
|
using Azure.Core;
|
||||||
|
using Azure.Identity;
|
||||||
|
using DotNetEnv;
|
||||||
|
using Hosted_Shared_Contributor_Setup;
|
||||||
|
using Microsoft.Agents.AI;
|
||||||
|
using Microsoft.Agents.AI.Foundry.Hosting;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
// Load .env file if present (for local development)
|
||||||
|
Env.TraversePath().Load();
|
||||||
|
|
||||||
|
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||||
|
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||||
|
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||||
|
string skillNames = Environment.GetEnvironmentVariable("SKILL_NAMES")
|
||||||
|
?? throw new InvalidOperationException("SKILL_NAMES is not set. Provide a comma-separated list of skill names (e.g., support-style,escalation-policy).");
|
||||||
|
|
||||||
|
string[] requestedSkills = skillNames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (requestedSkills.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("SKILL_NAMES must list at least one skill name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate skill names to prevent path traversal.
|
||||||
|
foreach (string name in requestedSkills)
|
||||||
|
{
|
||||||
|
if (name.Contains('.') || name.Contains('/') || name.Contains('\\') || Path.IsPathRooted(name))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Invalid skill name '{name}': skill names must not contain path separators or dots.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
|
||||||
|
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
|
||||||
|
TokenCredential credential = new ChainedTokenCredential(
|
||||||
|
new DevTemporaryTokenCredential(),
|
||||||
|
new DefaultAzureCredential());
|
||||||
|
|
||||||
|
AIProjectClient projectClient = new(new Uri(endpoint), credential);
|
||||||
|
ProjectAgentSkills skillsClient = projectClient.AgentAdministrationClient.GetAgentSkills();
|
||||||
|
|
||||||
|
// ── Provision skills (sample convenience only — NOT a production pattern) ─────
|
||||||
|
// In production, skills are provisioned externally (e.g., via CI/CD or a management script).
|
||||||
|
// This helper ensures the sample's SKILL.md files exist in Foundry so the sample is runnable
|
||||||
|
// out of the box without a separate setup step. Set PROVISION_SAMPLE_SKILLS=true to enable.
|
||||||
|
string sourceSkillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
|
||||||
|
bool provisionEnabled = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (provisionEnabled && Directory.Exists(sourceSkillsDir))
|
||||||
|
{
|
||||||
|
await EnsureSkillsProvisionedAsync(skillsClient, sourceSkillsDir, requestedSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Download skills from Foundry ─────────────────────────────────────────────
|
||||||
|
// Pull the latest copy of each skill from Foundry into a runtime-only folder.
|
||||||
|
// This directory is recreated on every startup so the agent always picks up
|
||||||
|
// the latest version of each skill.
|
||||||
|
string downloadedSkillsDir = Path.Combine(AppContext.BaseDirectory, "downloaded_skills");
|
||||||
|
await DownloadSkillsAsync(skillsClient, requestedSkills, downloadedSkillsDir);
|
||||||
|
|
||||||
|
// ── Wire skills into the agent ───────────────────────────────────────────────
|
||||||
|
// AgentSkillsProvider implements progressive disclosure: skill names and descriptions
|
||||||
|
// are advertised in the system prompt (~100 tokens per skill), and the full SKILL.md
|
||||||
|
// body is loaded on demand when the model calls the load_skill tool.
|
||||||
|
AgentSkillsProvider skillsProvider = new(downloadedSkillsDir);
|
||||||
|
|
||||||
|
ChatClientAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
|
||||||
|
{
|
||||||
|
Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills",
|
||||||
|
ChatOptions = new ChatOptions
|
||||||
|
{
|
||||||
|
ModelId = deploymentName,
|
||||||
|
Instructions = "You are a customer-support assistant for Contoso Outdoors.",
|
||||||
|
},
|
||||||
|
AIContextProviders = [skillsProvider]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Host the agent as a Foundry Hosted Agent using the Responses API.
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Services.AddFoundryResponses(agent);
|
||||||
|
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.MapFoundryResponses();
|
||||||
|
|
||||||
|
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
|
||||||
|
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
|
||||||
|
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
|
||||||
|
app.MapDevTemporaryLocalAgentEndpoint();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Downloads each named skill from Foundry and extracts the ZIP archive into a
|
||||||
|
// separate subdirectory under the target directory.
|
||||||
|
static async Task DownloadSkillsAsync(ProjectAgentSkills skillsClient, string[] skillNames, string targetDir)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(targetDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(targetDir, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(targetDir);
|
||||||
|
|
||||||
|
foreach (string name in skillNames)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Downloading skill '{name}' from Foundry...");
|
||||||
|
BinaryData zipData = await skillsClient.DownloadSkillAsync(name);
|
||||||
|
|
||||||
|
string skillDir = Path.Combine(targetDir, name);
|
||||||
|
Directory.CreateDirectory(skillDir);
|
||||||
|
|
||||||
|
using var zipStream = zipData.ToStream();
|
||||||
|
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
|
||||||
|
SafeExtractZip(archive, skillDir);
|
||||||
|
|
||||||
|
if (!File.Exists(Path.Combine(skillDir, "SKILL.md")))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Downloaded archive for '{name}' did not contain a SKILL.md at the root.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts a ZIP archive into a destination directory, rejecting entries that would
|
||||||
|
// escape the target path (zip-slip guard).
|
||||||
|
static void SafeExtractZip(ZipArchive archive, string destinationDir)
|
||||||
|
{
|
||||||
|
string destRoot = Path.GetFullPath(destinationDir);
|
||||||
|
string destRootWithSep = Path.EndsInDirectorySeparator(destRoot)
|
||||||
|
? destRoot
|
||||||
|
: destRoot + Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
// Use ordinal comparison on Unix (case-sensitive FS) and ordinal-ignore-case on Windows.
|
||||||
|
var comparison = OperatingSystem.IsWindows()
|
||||||
|
? StringComparison.OrdinalIgnoreCase
|
||||||
|
: StringComparison.Ordinal;
|
||||||
|
|
||||||
|
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||||
|
{
|
||||||
|
string entryPath = Path.GetFullPath(Path.Combine(destRoot, entry.FullName));
|
||||||
|
if (!entryPath.StartsWith(destRootWithSep, comparison)
|
||||||
|
&& !string.Equals(entryPath, destRoot, comparison))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Refusing to extract unsafe path '{entry.FullName}' outside of '{destRoot}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
// Directory entry — ensure it exists.
|
||||||
|
Directory.CreateDirectory(entryPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(entryPath)!);
|
||||||
|
entry.ExtractToFile(entryPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures each requested skill is provisioned in Foundry. For each skill name, checks whether
|
||||||
|
// the skill exists and uploads it from the local source directory if it does not.
|
||||||
|
//
|
||||||
|
// This is a sample convenience helper — in production, skill provisioning is an external concern.
|
||||||
|
static async Task EnsureSkillsProvisionedAsync(ProjectAgentSkills skillsClient, string sourceDir, string[] skillNames)
|
||||||
|
{
|
||||||
|
foreach (string name in skillNames)
|
||||||
|
{
|
||||||
|
string skillPath = Path.Combine(sourceDir, name);
|
||||||
|
if (!Directory.Exists(skillPath) || !File.Exists(Path.Combine(skillPath, "SKILL.md")))
|
||||||
|
{
|
||||||
|
continue; // No local source for this skill — skip provisioning.
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await skillsClient.GetSkillAsync(name);
|
||||||
|
Console.WriteLine($"Skill '{name}' already exists in Foundry.");
|
||||||
|
}
|
||||||
|
catch (ClientResultException ex) when (ex.Status == 404)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Provisioning skill '{name}' from {skillPath}...");
|
||||||
|
AgentsSkill imported = await skillsClient.CreateSkillFromPackageAsync(skillPath);
|
||||||
|
Console.WriteLine($" Imported skill '{imported.Name}' (id={imported.SkillId}, has_blob={imported.HasBlob}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# What this sample demonstrates
|
||||||
|
|
||||||
|
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through the Skills REST API, and downloaded by the agent on boot so updates ship without code changes.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Authoring skills
|
||||||
|
|
||||||
|
Each skill is a Markdown file with a YAML front matter block. This sample ships two source skills under [`skills/`](skills/):
|
||||||
|
|
||||||
|
| Skill | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. |
|
||||||
|
| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket. |
|
||||||
|
|
||||||
|
Each `SKILL.md` includes a unique `*-CANARY-*` token that the model is asked to echo, so you can prove the skill was loaded from Foundry (not hallucinated) by checking the response.
|
||||||
|
|
||||||
|
> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills REST API to return HTTP 500 on import.
|
||||||
|
|
||||||
|
### Uploading skills
|
||||||
|
|
||||||
|
The sample includes a convenience provisioning step that checks whether each skill exists in Foundry and uploads it if not, gated behind the `PROVISION_SAMPLE_SKILLS=true` env var. **In production, skill provisioning is an external concern** — it is NOT the hosted agent's responsibility. A real deployment pipeline would provision skills separately (e.g., via a CI/CD step, a CLI script, or a management portal).
|
||||||
|
|
||||||
|
The provisioning uses `ProjectAgentSkills.CreateSkillFromPackageAsync(directoryPath)` from the `Azure.AI.Projects.Agents` SDK. The method packages the `SKILL.md` file as a ZIP and uploads it to Foundry.
|
||||||
|
|
||||||
|
### Downloading skills at agent startup
|
||||||
|
|
||||||
|
[`Program.cs`](Program.cs) reads the comma-separated `SKILL_NAMES` env var and for each skill name downloads the ZIP archive from Foundry via `ProjectAgentSkills.DownloadSkillAsync(name)`, then unpacks it into a **separate runtime directory** at `downloaded_skills/<name>/` (kept distinct from the static `skills/` source folder).
|
||||||
|
|
||||||
|
An [`AgentSkillsProvider`](../../../../../src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs) is then built over `downloaded_skills/` and attached to the agent as a context provider. The provider follows the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern:
|
||||||
|
|
||||||
|
1. **Advertise** — skill names and descriptions are injected into the system prompt at session start (~100 tokens per skill).
|
||||||
|
2. **Load** — the model calls the `load_skill` tool when it decides a skill is relevant to the user's turn, and the full `SKILL.md` body is returned.
|
||||||
|
|
||||||
|
This means the model only pays the token cost for a skill's full body when it actually needs it, and updating a skill in Foundry + restarting the agent is enough to pick up the change — no code redeploy required.
|
||||||
|
|
||||||
|
> **Note:** This sample supports instruction-only and resource-based skills. If your downloaded skills contain scripts, add a script runner when constructing the `AgentSkillsProvider`.
|
||||||
|
|
||||||
|
### Agent Hosting
|
||||||
|
|
||||||
|
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the Responses API hosting layer (`AddFoundryResponses` / `MapFoundryResponses`).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
|
||||||
|
- Azure CLI logged in (`az login`)
|
||||||
|
|
||||||
|
### Required RBAC
|
||||||
|
|
||||||
|
Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both authoring skills and downloading them.
|
||||||
|
|
||||||
|
## Running the Agent Host
|
||||||
|
|
||||||
|
Set the required environment variables and run the sample with `dotnet run`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AZURE_AI_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
|
||||||
|
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o"
|
||||||
|
export SKILL_NAMES="support-style,escalation-policy"
|
||||||
|
export PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:SKILL_NAMES="support-style,escalation-policy"
|
||||||
|
$env:PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also place these in a `.env` file next to `Program.cs` — see [`.env.example`](.env.example).
|
||||||
|
|
||||||
|
On startup you should see:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Skill 'support-style' already exists in Foundry.
|
||||||
|
Skill 'escalation-policy' already exists in Foundry.
|
||||||
|
Downloading skill 'support-style' from Foundry...
|
||||||
|
Downloading skill 'escalation-policy' from Foundry...
|
||||||
|
```
|
||||||
|
|
||||||
|
The downloaded `SKILL.md` files land under `downloaded_skills/<name>/SKILL.md` next to the published output. This directory is recreated from scratch on every run, so deleting it manually is never necessary.
|
||||||
|
|
||||||
|
## Interacting with the agent
|
||||||
|
|
||||||
|
> Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi, I am Alex. I just want to confirm I can return my tent within 30 days."}'
|
||||||
|
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}'
|
||||||
|
```
|
||||||
|
|
||||||
|
| Prompt mentions | Skill that should drive the response |
|
||||||
|
|---|---|
|
||||||
|
| Routine return / shipping / care question | Model loads `support-style` (canary `STYLE-CANARY-3318`) — no escalation. |
|
||||||
|
| Injury, legal threat, press, or refund > $500 | Model loads `escalation-policy` (canary `ESC-CANARY-7742`) **and** `support-style`. |
|
||||||
|
|
||||||
|
Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill (not just saw its name in the advertised list).
|
||||||
|
|
||||||
|
## Deploying the Agent to Foundry
|
||||||
|
|
||||||
|
When deploying to Foundry, make sure `SKILL_NAMES` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
azd env set SKILL_NAMES "support-style,escalation-policy"
|
||||||
|
```
|
||||||
|
|
||||||
|
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup.
|
||||||
|
|
||||||
|
> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The provisioning step must have been run against the same Foundry project before the agent can download the skills.
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
|
||||||
|
name: hosted-agent-skills
|
||||||
|
displayName: "Hosted Agent Skills"
|
||||||
|
|
||||||
|
description: >
|
||||||
|
An Agent Framework agent that downloads its behavioral guidelines from the Foundry
|
||||||
|
Skills REST API at startup, demonstrating how to decouple behavioral guidelines
|
||||||
|
(tone, escalation policy, etc.) from agent code using AgentSkillsProvider.
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
tags:
|
||||||
|
- AI Agent Hosting
|
||||||
|
- Azure AI AgentServer
|
||||||
|
- Responses Protocol
|
||||||
|
- Agent Framework
|
||||||
|
- Agent Skills
|
||||||
|
- Foundry Skills
|
||||||
|
|
||||||
|
template:
|
||||||
|
name: hosted-agent-skills
|
||||||
|
kind: hosted
|
||||||
|
protocols:
|
||||||
|
- protocol: responses
|
||||||
|
version: 1.0.0
|
||||||
|
resources:
|
||||||
|
cpu: "0.25"
|
||||||
|
memory: 0.5Gi
|
||||||
|
environment_variables:
|
||||||
|
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||||
|
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||||
|
- name: SKILL_NAMES
|
||||||
|
value: "{{SKILL_NAMES}}"
|
||||||
|
parameters:
|
||||||
|
properties:
|
||||||
|
- name: SKILL_NAMES
|
||||||
|
secret: false
|
||||||
|
description: Comma-separated list of Foundry skill names to download at startup (e.g., support-style,escalation-policy)
|
||||||
|
resources:
|
||||||
|
- kind: model
|
||||||
|
id: gpt-4.1-mini
|
||||||
|
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||||
|
kind: hosted
|
||||||
|
name: hosted-agent-skills
|
||||||
|
protocols:
|
||||||
|
- protocol: responses
|
||||||
|
version: 1.0.0
|
||||||
|
resources:
|
||||||
|
cpu: "0.25"
|
||||||
|
memory: 0.5Gi
|
||||||
|
environment_variables:
|
||||||
|
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||||
|
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
|
||||||
|
- name: SKILL_NAMES
|
||||||
|
value: ${SKILL_NAMES}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#requires -Version 7
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Local smoke test for the Hosted-AgentSkills sample.
|
||||||
|
.DESCRIPTION
|
||||||
|
Publishes the sample, builds the contributor Docker image, runs the container, drives
|
||||||
|
two conversations via curl invocations, and asserts that the agent loaded the correct
|
||||||
|
Foundry Skill for each prompt (verified via canary tokens in the response).
|
||||||
|
Exits non-zero on failure.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
- Docker
|
||||||
|
- az login (token is fetched from the host)
|
||||||
|
- .env populated with AZURE_AI_PROJECT_ENDPOINT and model deployment
|
||||||
|
- Skills provisioned to Foundry (set PROVISION_SAMPLE_SKILLS=true on first run)
|
||||||
|
.NOTES
|
||||||
|
This script is for local Docker debugging only. The Foundry platform supplies the
|
||||||
|
isolation keys for every inbound request in production and the dev fallback used here
|
||||||
|
must not be enabled in production deployments.
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$Port = 8088,
|
||||||
|
[string]$ImageName = 'hosted-agent-skills-smoke',
|
||||||
|
[string]$ContainerName = 'hosted-agent-skills-smoke'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-Location -Path $PSScriptRoot/..
|
||||||
|
|
||||||
|
if (-not (Test-Path .env)) {
|
||||||
|
throw '.env not found. Copy .env.example to .env and fill in AZURE_AI_PROJECT_ENDPOINT.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '==> Publishing sample for linux-musl-x64 ...'
|
||||||
|
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off | Out-Host
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'dotnet publish failed.' }
|
||||||
|
|
||||||
|
Write-Host '==> Building docker image ...'
|
||||||
|
docker build -f Dockerfile.contributor -t $ImageName . | Out-Host
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'docker build failed.' }
|
||||||
|
|
||||||
|
Write-Host '==> Fetching bearer token ...'
|
||||||
|
$bearer = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
|
||||||
|
if (-not $bearer) { throw 'Failed to obtain bearer token. Run az login.' }
|
||||||
|
|
||||||
|
function Start-Container {
|
||||||
|
docker rm -f $ContainerName 2>$null | Out-Null
|
||||||
|
docker run -d --name $ContainerName -p ${Port}:8088 `
|
||||||
|
-e AGENT_NAME=hosted-agent-skills `
|
||||||
|
-e AZURE_BEARER_TOKEN=$bearer `
|
||||||
|
-e HOSTED_USER_ISOLATION_KEY=smoke-user `
|
||||||
|
-e HOSTED_CHAT_ISOLATION_KEY=smoke-chat-1 `
|
||||||
|
--env-file .env `
|
||||||
|
$ImageName | Out-Host
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "docker run failed." }
|
||||||
|
# Wait for the server to start and download skills from Foundry.
|
||||||
|
Write-Host ' Waiting for startup (skill download + server ready) ...'
|
||||||
|
Start-Sleep -Seconds 15
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Agent([string]$Prompt, [string]$PreviousResponseId = $null) {
|
||||||
|
$body = @{ input = $Prompt; model = 'hosted-agent-skills' }
|
||||||
|
if ($PreviousResponseId) { $body['previous_response_id'] = $PreviousResponseId }
|
||||||
|
$json = $body | ConvertTo-Json -Compress
|
||||||
|
$resp = Invoke-RestMethod -Method Post -Uri "http://localhost:$Port/responses" -ContentType 'application/json' -Body $json
|
||||||
|
return $resp
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ResponseText($response) {
|
||||||
|
return ($response.output | ForEach-Object { $_.content | ForEach-Object { $_.text } }) -join ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-Contains([string]$Haystack, [string]$Needle, [string]$Label) {
|
||||||
|
if ($Haystack -notmatch [regex]::Escape($Needle)) {
|
||||||
|
throw "FAILED [$Label]: expected response to contain '$Needle' but got: $Haystack"
|
||||||
|
}
|
||||||
|
Write-Host "PASS [$Label]: response contains '$Needle'."
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Start-Container
|
||||||
|
|
||||||
|
Write-Host '==> Test 1: Routine support question -> support-style skill ...'
|
||||||
|
$r1 = Invoke-Agent -Prompt 'Hi, I am Alex. I just want to confirm I can return my tent within 30 days.'
|
||||||
|
$text1 = Get-ResponseText $r1
|
||||||
|
Assert-Contains $text1 'STYLE-CANARY-3318' 'routine question: support-style canary'
|
||||||
|
|
||||||
|
Write-Host '==> Test 2: Escalation trigger -> escalation-policy skill ...'
|
||||||
|
$r2 = Invoke-Agent -Prompt 'I want a $750 refund on Order #A-1042 right now or I am calling my lawyer.'
|
||||||
|
$text2 = Get-ResponseText $r2
|
||||||
|
Assert-Contains $text2 'ESC-CANARY-7742' 'escalation trigger: escalation-policy canary'
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '==> All smoke assertions passed.'
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
docker rm -f $ContainerName 2>$null | Out-Null
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: escalation-policy
|
||||||
|
description: When and how to escalate Contoso Outdoors customer-support tickets.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Contoso Outdoors Escalation Policy
|
||||||
|
|
||||||
|
You must follow this escalation policy on every conversation.
|
||||||
|
|
||||||
|
## Escalate immediately when the customer
|
||||||
|
|
||||||
|
- Reports an injury, allergic reaction, or other safety incident.
|
||||||
|
- Mentions legal action, regulators, or the press.
|
||||||
|
- Has waited more than 14 days for a refund that was already approved.
|
||||||
|
- Requests a refund larger than $500.
|
||||||
|
|
||||||
|
## How to escalate
|
||||||
|
|
||||||
|
1. Acknowledge the issue in one sentence.
|
||||||
|
2. Tell the customer you are escalating to a senior specialist.
|
||||||
|
3. Provide the escalation reference `ESC-CANARY-7742` and the SLA: a senior
|
||||||
|
specialist will reply within 1 business day.
|
||||||
|
4. Do not promise a specific outcome (refund, replacement, compensation) on
|
||||||
|
escalated tickets — only the senior specialist can commit to one.
|
||||||
|
|
||||||
|
## Do not escalate
|
||||||
|
|
||||||
|
- Routine returns within the standard 30-day window.
|
||||||
|
- Shipping status questions.
|
||||||
|
- Product care and usage questions.
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: support-style
|
||||||
|
description: Contoso Outdoors customer-support tone and formatting guidelines.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Contoso Outdoors Support Style
|
||||||
|
|
||||||
|
You are speaking on behalf of Contoso Outdoors customer support.
|
||||||
|
|
||||||
|
## Voice
|
||||||
|
|
||||||
|
- Warm, concise, and confident — never apologetic in a hand-wringing way.
|
||||||
|
- Use the customer's name when it is known.
|
||||||
|
- Sign every response with `— Contoso Outdoors Support`.
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
- Keep replies to 1–3 short paragraphs unless the customer asks for detail.
|
||||||
|
- Use bullet lists only when enumerating concrete steps or options.
|
||||||
|
- Always reference order numbers as `Order #<id>` (e.g. `Order #A-1042`).
|
||||||
|
|
||||||
|
## Canary
|
||||||
|
|
||||||
|
To prove this skill was loaded, include the token `STYLE-CANARY-3318` on a
|
||||||
|
separate line at the bottom of every response, prefixed with `# `.
|
||||||
+3
-3
@@ -27,9 +27,9 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Azure.AI.Projects" />
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Azure.AI.Projects" />
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -13,7 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Azure.AI.Projects" />
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
<PackageReference Include="ModelContextProtocol" VersionOverride="1.2.0" />
|
<PackageReference Include="ModelContextProtocol" />
|
||||||
<PackageReference Include="DotNetEnv" />
|
<PackageReference Include="DotNetEnv" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -26,9 +26,9 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||||
|
ASPNETCORE_URLS=http://+:8088
|
||||||
|
ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5
|
||||||
|
FOUNDRY_TOOLBOX_NAME=<your-toolbox-name>
|
||||||
|
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Dockerfile for end-users consuming the Agent Framework via NuGet packages.
|
||||||
|
#
|
||||||
|
# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container,
|
||||||
|
# which only succeeds when the project references its dependencies via PackageReference (see the
|
||||||
|
# commented-out section in HostedToolboxMcpSkills.csproj). Contributors building from the
|
||||||
|
# agent-framework repository source must use Dockerfile.contributor instead because
|
||||||
|
# ProjectReference dependencies live outside this folder and cannot be restored from inside
|
||||||
|
# this build context.
|
||||||
|
#
|
||||||
|
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet restore
|
||||||
|
RUN dotnet publish -c Release -o /app/publish
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
EXPOSE 8088
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8088
|
||||||
|
ENTRYPOINT ["dotnet", "HostedToolboxMcpSkills.dll"]
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Dockerfile for contributors building from the agent-framework repository source.
|
||||||
|
#
|
||||||
|
# This project uses ProjectReference to the local source, which means a standard
|
||||||
|
# multi-stage Docker build cannot resolve dependencies outside this folder.
|
||||||
|
# Pre-publish the app targeting the container runtime and copy the output:
|
||||||
|
#
|
||||||
|
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||||
|
# docker build -f Dockerfile.contributor -t hosted-toolbox-mcp-skills .
|
||||||
|
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-toolbox-mcp-skills -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-toolbox-mcp-skills
|
||||||
|
#
|
||||||
|
# For end-users consuming the NuGet package (not ProjectReference), use the standard
|
||||||
|
# Dockerfile which performs a full dotnet restore + publish inside the container.
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY out/ .
|
||||||
|
EXPOSE 8088
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8088
|
||||||
|
ENTRYPOINT ["dotnet", "HostedToolboxMcpSkills.dll"]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user