Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 848443ac68 | |||
| 1466d68cf1 | |||
| d08200d00e | |||
| fb38b1d10a | |||
| a70fe21298 | |||
| f6a3c43e9a | |||
| e6f7b3e9be | |||
| a1f3e536bc | |||
| c033adb1f4 | |||
| 09473fa7ed | |||
| a4f02aabf0 | |||
| afdf8af400 | |||
| 9e836f7b42 | |||
| 9cf5143321 | |||
| b6b16ddb75 | |||
| c218067646 | |||
| ac474100ce | |||
| a057cd505c | |||
| c66bb39ea2 | |||
| 7c6b1e975f | |||
| 0d2925037d | |||
| b5e635ed4d | |||
| 1036fa7438 | |||
| 62da382082 | |||
| 3604ba70f6 | |||
| 3ab2630243 | |||
| bc59c72170 | |||
| d5f2c77b35 | |||
| 6afae2f9b4 | |||
| cad81923e3 | |||
| 5ab8877ba5 | |||
| f4e49958f3 | |||
| 5282c158aa | |||
| dde7635760 | |||
| 85c00fc55b | |||
| f19a129b55 | |||
| a376577263 | |||
| b2549337ff | |||
| 05834b56e3 | |||
| 42ae534a07 | |||
| a17102f9f5 | |||
| b3f2e53923 | |||
| b5300fe0c0 | |||
| c35a63ed8d | |||
| 47cd0a508d | |||
| 56e9a8f74c | |||
| ba0ad2d1d2 | |||
| b123480b65 | |||
| 4c0d9ed43c | |||
| 1c0082721c | |||
| 6c0950adeb | |||
| f1ba16e3fd | |||
| cba77e3cd0 | |||
| 7ca8bb55b6 | |||
| d93fc2dd74 | |||
| 54617557e6 | |||
| df198005fd | |||
| 0ceca9a76a | |||
| 23977a6045 | |||
| 18b03ea487 | |||
| 56c4425db2 | |||
| 13066cdf96 | |||
| f11cfd9d76 | |||
| 774fc94bd2 | |||
| 4bac2c2c05 | |||
| 43568f1ef2 | |||
| 52005ff17d | |||
| a4e4a5a51c | |||
| c8fb491644 | |||
| e57f046d8a | |||
| c9b19e831f | |||
| beb65b21a8 | |||
| b3d523ee50 | |||
| 6f38cb724d | |||
| 8e74360d52 | |||
| 7f4cc296fd | |||
| f3057ef20c | |||
| 68136ee081 | |||
| 875031ff56 | |||
| 87af313119 | |||
| 9ac548ad15 | |||
| 737042fc93 | |||
| e677ccc3b1 | |||
| d9c0c36379 | |||
| 32a547a1a7 |
@@ -17,7 +17,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
@@ -46,4 +46,4 @@ runs:
|
||||
- name: Install the project
|
||||
shell: bash
|
||||
run: |
|
||||
cd python && uv sync --all-packages --all-extras --dev --prerelease=if-necessary-or-explicit
|
||||
cd python && uv sync --all-packages --all-extras --all-groups --prerelease=if-necessary-or-explicit
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/**
|
||||
* Resolve the issue author and check their team membership.
|
||||
* Resolve the issue or pull request author and check their team membership.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.github - Octokit REST client from actions/github-script
|
||||
* @param {object} opts.context - GitHub Actions context
|
||||
* @param {object} opts.core - GitHub Actions core toolkit
|
||||
* @param {string} opts.teamSlug - Team slug to check membership against
|
||||
* @param {string|number} opts.issueNumber - Issue number to resolve author for
|
||||
* @param {string|number} opts.issueNumber - Issue or pull request number to resolve author for
|
||||
* @returns {Promise<{author: string|null, isTeamMember: boolean}>}
|
||||
*/
|
||||
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber }) {
|
||||
let author = context.payload.issue?.user?.login;
|
||||
let author =
|
||||
context.payload.issue?.user?.login ??
|
||||
context.payload.pull_request?.user?.login;
|
||||
|
||||
if (!author) {
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number(issueNumber),
|
||||
});
|
||||
author = issue.user?.login;
|
||||
const number = Number(issueNumber);
|
||||
if (context.payload.pull_request) {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: number,
|
||||
});
|
||||
author = pr.user?.login;
|
||||
} else {
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: number,
|
||||
});
|
||||
author = issue.user?.login;
|
||||
}
|
||||
}
|
||||
|
||||
if (!author) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
|
||||
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
||||
const BRANCH_PATTERN = /^[a-zA-Z0-9_./-]+$/;
|
||||
|
||||
function assertValidSha(sha, description) {
|
||||
if (!SHA_PATTERN.test(sha)) {
|
||||
throw new Error(`GitHub returned an invalid ${description} SHA.`);
|
||||
}
|
||||
}
|
||||
|
||||
function hasWritePermission(permissionData) {
|
||||
return permissionData.user?.permissions?.push === true
|
||||
|| ['admin', 'maintain', 'write'].includes(permissionData.permission);
|
||||
}
|
||||
|
||||
function latestDecisiveReviews(reviews) {
|
||||
const latestByReviewer = new Map();
|
||||
const sortedReviews = [...reviews].sort((left, right) => {
|
||||
const submittedComparison = (left.submitted_at || '').localeCompare(right.submitted_at || '');
|
||||
return submittedComparison || Number(left.id) - Number(right.id);
|
||||
});
|
||||
|
||||
for (const review of sortedReviews) {
|
||||
const state = review.state?.toUpperCase();
|
||||
const reviewer = review.user?.login?.toLowerCase();
|
||||
if (reviewer && DECISIVE_REVIEW_STATES.has(state)) {
|
||||
latestByReviewer.set(reviewer, review);
|
||||
}
|
||||
}
|
||||
|
||||
return latestByReviewer;
|
||||
}
|
||||
|
||||
async function resolvePullRequest({ github, context, core, prNumber, requiredApprovals }) {
|
||||
if (!/^[0-9]+$/.test(prNumber)) {
|
||||
throw new Error('Invalid PR number. Only numeric values are allowed.');
|
||||
}
|
||||
|
||||
const pullNumber = Number(prNumber);
|
||||
const { data: pullRequest } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
|
||||
if (pullRequest.state !== 'open') {
|
||||
throw new Error(`PR #${pullNumber} is not open (state: ${pullRequest.state}).`);
|
||||
}
|
||||
|
||||
const headSha = pullRequest.head.sha;
|
||||
const baseSha = pullRequest.base.sha;
|
||||
assertValidSha(headSha, 'PR head');
|
||||
assertValidSha(baseSha, 'PR base');
|
||||
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, {
|
||||
...context.repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const latestReviews = latestDecisiveReviews(reviews);
|
||||
const author = pullRequest.user?.login?.toLowerCase();
|
||||
const approvalCandidates = [...latestReviews.entries()]
|
||||
.filter(([, review]) => review.state.toUpperCase() === 'APPROVED')
|
||||
.filter(([, review]) => review.commit_id === headSha)
|
||||
.filter(([reviewer]) => reviewer !== author);
|
||||
|
||||
const approvedMaintainers = [];
|
||||
for (const [reviewer] of approvalCandidates) {
|
||||
const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
...context.repo,
|
||||
username: reviewer,
|
||||
});
|
||||
if (hasWritePermission(permissionData)) {
|
||||
approvedMaintainers.push(reviewer);
|
||||
} else {
|
||||
core.info(`Ignoring approval from ${reviewer}: reviewer does not have write permission.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (approvedMaintainers.length < requiredApprovals) {
|
||||
throw new Error(
|
||||
`PR #${pullNumber} head ${headSha} requires ${requiredApprovals} approvals from unique `
|
||||
+ `write-capable maintainers; found ${approvedMaintainers.length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
core.info(
|
||||
`PR #${pullNumber} head ${headSha} approved by: ${approvedMaintainers.join(', ')}.`,
|
||||
);
|
||||
return {
|
||||
baseRef: baseSha,
|
||||
checkoutRef: headSha,
|
||||
description: `PR #${pullNumber}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveBranch({ github, context, core, branch }) {
|
||||
if (!BRANCH_PATTERN.test(branch)) {
|
||||
throw new Error(
|
||||
'Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes '
|
||||
+ 'are allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
const [{ data: repository }, { data: targetBranch }] = await Promise.all([
|
||||
github.rest.repos.get(context.repo),
|
||||
github.rest.repos.getBranch({ ...context.repo, branch }),
|
||||
]);
|
||||
const { data: baseBranch } = await github.rest.repos.getBranch({
|
||||
...context.repo,
|
||||
branch: repository.default_branch,
|
||||
});
|
||||
|
||||
const checkoutRef = targetBranch.commit.sha;
|
||||
const baseRef = baseBranch.commit.sha;
|
||||
assertValidSha(checkoutRef, 'branch head');
|
||||
assertValidSha(baseRef, 'default branch');
|
||||
core.info(`Branch ${branch} resolved to immutable commit ${checkoutRef}.`);
|
||||
|
||||
return {
|
||||
baseRef,
|
||||
checkoutRef,
|
||||
description: `branch ${branch}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a manually requested integration-test target to an immutable commit.
|
||||
*
|
||||
* Pull requests must have fresh approvals from two unique write-capable
|
||||
* maintainers for the exact head commit. Branches are limited to branches in
|
||||
* the base repository and are pinned to their current commit.
|
||||
*/
|
||||
async function resolveIntegrationTestTarget({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber = '',
|
||||
branch = '',
|
||||
requiredApprovals = 2,
|
||||
}) {
|
||||
const normalizedPrNumber = prNumber.trim();
|
||||
const normalizedBranch = branch.trim();
|
||||
|
||||
if (normalizedPrNumber && normalizedBranch) {
|
||||
throw new Error('Please provide either a PR number or a branch name, not both.');
|
||||
}
|
||||
if (!normalizedPrNumber && !normalizedBranch) {
|
||||
throw new Error('Please provide either a PR number or a branch name.');
|
||||
}
|
||||
|
||||
if (normalizedPrNumber) {
|
||||
return resolvePullRequest({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber: normalizedPrNumber,
|
||||
requiredApprovals,
|
||||
});
|
||||
}
|
||||
return resolveBranch({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
branch: normalizedBranch,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = resolveIntegrationTestTarget;
|
||||
@@ -16,7 +16,12 @@ const checkTeamMembership = require('../scripts/check_team_membership.js');
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState = 'active' } = {}) {
|
||||
function createMocks({
|
||||
payloadIssue = undefined,
|
||||
payloadPullRequest = undefined,
|
||||
apiUser = 'api-user',
|
||||
teamState = 'active',
|
||||
} = {}) {
|
||||
const core = {
|
||||
_infoMessages: [],
|
||||
_failedMessages: [],
|
||||
@@ -24,8 +29,16 @@ function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState
|
||||
setFailed(msg) { this._failedMessages.push(msg); },
|
||||
};
|
||||
|
||||
const payload = {};
|
||||
if (payloadIssue !== undefined) {
|
||||
payload.issue = payloadIssue;
|
||||
}
|
||||
if (payloadPullRequest !== undefined) {
|
||||
payload.pull_request = payloadPullRequest;
|
||||
}
|
||||
|
||||
const context = {
|
||||
payload: { issue: payloadIssue },
|
||||
payload,
|
||||
repo: { owner: 'test-org', repo: 'test-repo' },
|
||||
};
|
||||
|
||||
@@ -36,6 +49,11 @@ function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState
|
||||
data: { user: apiUser ? { login: apiUser } : null },
|
||||
}),
|
||||
},
|
||||
pulls: {
|
||||
get: async () => ({
|
||||
data: { user: apiUser ? { login: apiUser } : null },
|
||||
}),
|
||||
},
|
||||
teams: {
|
||||
getByName: async () => ({}),
|
||||
getMembershipForUserInOrg: async () => ({
|
||||
@@ -64,6 +82,37 @@ describe('author resolution', () => {
|
||||
assert.equal(result.author, 'payload-user');
|
||||
});
|
||||
|
||||
it('resolves author from pull_request event payload', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadPullRequest: { user: { login: 'pr-author' } },
|
||||
});
|
||||
let issuesGetCalled = false;
|
||||
github.rest.issues.get = async () => {
|
||||
issuesGetCalled = true;
|
||||
return { data: { user: { login: 'api-user' } } };
|
||||
};
|
||||
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.author, 'pr-author');
|
||||
assert.equal(issuesGetCalled, false);
|
||||
});
|
||||
|
||||
it('resolves author via pulls API when pull_request payload user is null', async () => {
|
||||
const { github, context, core } = createMocks({
|
||||
payloadPullRequest: { user: null },
|
||||
apiUser: 'fetched-pr-author',
|
||||
});
|
||||
let pullsGetCalled = false;
|
||||
github.rest.pulls.get = async () => {
|
||||
pullsGetCalled = true;
|
||||
return { data: { user: { login: 'fetched-pr-author' } } };
|
||||
};
|
||||
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
assert.equal(result.author, 'fetched-pr-author');
|
||||
assert.equal(pullsGetCalled, true);
|
||||
});
|
||||
|
||||
it('resolves author via API when payload issue is absent', async () => {
|
||||
const { github, context, core } = createMocks({ apiUser: 'api-user' });
|
||||
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/**
|
||||
* Tests for resolve_integration_test_target.js.
|
||||
*
|
||||
* Run with: node --test .github/tests/test_resolve_integration_test_target.js
|
||||
*/
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const resolveIntegrationTestTarget = require('../scripts/resolve_integration_test_target.js');
|
||||
|
||||
const HEAD_SHA = 'a'.repeat(40);
|
||||
const BASE_SHA = 'b'.repeat(40);
|
||||
|
||||
function review({
|
||||
id,
|
||||
login,
|
||||
state = 'APPROVED',
|
||||
commitId = HEAD_SHA,
|
||||
submittedAt = `2026-07-13T00:00:${String(id).padStart(2, '0')}Z`,
|
||||
}) {
|
||||
return {
|
||||
id,
|
||||
state,
|
||||
commit_id: commitId,
|
||||
submitted_at: submittedAt,
|
||||
user: { login },
|
||||
};
|
||||
}
|
||||
|
||||
function createMocks({
|
||||
pullState = 'open',
|
||||
pullAuthor = 'contributor',
|
||||
reviews = [],
|
||||
permissions = {},
|
||||
} = {}) {
|
||||
const core = {
|
||||
infoMessages: [],
|
||||
info(message) {
|
||||
this.infoMessages.push(message);
|
||||
},
|
||||
};
|
||||
const context = {
|
||||
repo: { owner: 'microsoft', repo: 'agent-framework' },
|
||||
};
|
||||
const github = {
|
||||
paginate: async () => reviews,
|
||||
rest: {
|
||||
pulls: {
|
||||
get: async () => ({
|
||||
data: {
|
||||
state: pullState,
|
||||
user: { login: pullAuthor },
|
||||
head: { sha: HEAD_SHA },
|
||||
base: { sha: BASE_SHA },
|
||||
},
|
||||
}),
|
||||
listReviews: async () => {},
|
||||
},
|
||||
repos: {
|
||||
get: async () => ({ data: { default_branch: 'main' } }),
|
||||
getBranch: async ({ branch }) => ({
|
||||
data: { commit: { sha: branch === 'main' ? BASE_SHA : HEAD_SHA } },
|
||||
}),
|
||||
getCollaboratorPermissionLevel: async ({ username }) => ({
|
||||
data: permissions[username] || {
|
||||
permission: 'read',
|
||||
user: { permissions: { push: false } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { core, context, github };
|
||||
}
|
||||
|
||||
const WRITE_PERMISSION = {
|
||||
permission: 'write',
|
||||
user: { permissions: { push: true } },
|
||||
};
|
||||
|
||||
describe('input validation', () => {
|
||||
it('rejects missing and conflicting targets', async () => {
|
||||
const mocks = createMocks();
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget(mocks),
|
||||
/provide either a PR number or a branch name/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '1', branch: 'feature' }),
|
||||
/not both/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid PR numbers and branch names', async () => {
|
||||
const mocks = createMocks();
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '1;echo' }),
|
||||
/Invalid PR number/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget({ ...mocks, branch: 'feature branch' }),
|
||||
/Invalid branch name/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pull request resolution', () => {
|
||||
it('pins an open PR with two fresh write-capable approvals', async () => {
|
||||
const mocks = createMocks({
|
||||
reviews: [
|
||||
review({ id: 1, login: 'maintainer-one' }),
|
||||
review({ id: 2, login: 'maintainer-two' }),
|
||||
],
|
||||
permissions: {
|
||||
'maintainer-one': WRITE_PERMISSION,
|
||||
'maintainer-two': WRITE_PERMISSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resolveIntegrationTestTarget({ ...mocks, prNumber: '123' });
|
||||
|
||||
assert.deepEqual(result, {
|
||||
baseRef: BASE_SHA,
|
||||
checkoutRef: HEAD_SHA,
|
||||
description: 'PR #123',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects closed PRs', async () => {
|
||||
const mocks = createMocks({ pullState: 'closed' });
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
|
||||
/is not open/,
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores stale, self, and read-only approvals', async () => {
|
||||
const mocks = createMocks({
|
||||
reviews: [
|
||||
review({ id: 1, login: 'stale', commitId: 'c'.repeat(40) }),
|
||||
review({ id: 2, login: 'contributor' }),
|
||||
review({ id: 3, login: 'reader' }),
|
||||
review({ id: 4, login: 'maintainer' }),
|
||||
],
|
||||
permissions: {
|
||||
contributor: WRITE_PERMISSION,
|
||||
reader: { permission: 'read', user: { permissions: { push: false } } },
|
||||
maintainer: WRITE_PERMISSION,
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
|
||||
/found 1/,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses each reviewer latest decisive review and ignores later comments', async () => {
|
||||
const mocks = createMocks({
|
||||
reviews: [
|
||||
review({ id: 1, login: 'changes-requested' }),
|
||||
review({ id: 2, login: 'changes-requested', state: 'CHANGES_REQUESTED' }),
|
||||
review({ id: 3, login: 'maintainer-one' }),
|
||||
review({ id: 4, login: 'maintainer-one', state: 'COMMENTED' }),
|
||||
review({ id: 5, login: 'maintainer-two' }),
|
||||
],
|
||||
permissions: {
|
||||
'changes-requested': WRITE_PERMISSION,
|
||||
'maintainer-one': WRITE_PERMISSION,
|
||||
'maintainer-two': WRITE_PERMISSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resolveIntegrationTestTarget({ ...mocks, prNumber: '123' });
|
||||
assert.equal(result.checkoutRef, HEAD_SHA);
|
||||
});
|
||||
|
||||
it('does not count a dismissed approval', async () => {
|
||||
const mocks = createMocks({
|
||||
reviews: [
|
||||
review({ id: 1, login: 'dismissed', state: 'DISMISSED' }),
|
||||
review({ id: 2, login: 'maintainer' }),
|
||||
],
|
||||
permissions: {
|
||||
dismissed: WRITE_PERMISSION,
|
||||
maintainer: WRITE_PERMISSION,
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
|
||||
/found 1/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('branch resolution', () => {
|
||||
it('pins base-repository branches and their comparison base to SHAs', async () => {
|
||||
const mocks = createMocks();
|
||||
const result = await resolveIntegrationTestTarget({ ...mocks, branch: 'feature/test' });
|
||||
|
||||
assert.deepEqual(result, {
|
||||
baseRef: BASE_SHA,
|
||||
checkoutRef: HEAD_SHA,
|
||||
description: 'branch feature/test',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,16 +23,16 @@ For each project that needs to be migrated, you need to do the following:
|
||||
- Identify the specific Semantic Kernel agent types being used:
|
||||
- `ChatCompletionAgent` → `ChatClientAgent`
|
||||
- `OpenAIAssistantAgent` → `assistantsClient.CreateAIAgent()` (via OpenAI Assistants client extension)
|
||||
- `AzureAIAgent` → `persistentAgentsClient.CreateAIAgent()` (via Azure AI Foundry client extension)
|
||||
- `AzureAIAgent` → `persistentAgentsClient.CreateAIAgent()` (via Microsoft Foundry client extension)
|
||||
- `OpenAIResponseAgent` → `responsesClient.CreateAIAgent()` (via OpenAI Responses client extension)
|
||||
- `A2AAgent` → `AIAgent` (via A2A card resolver)
|
||||
- `BedrockAgent` → Custom implementation required (not supported)
|
||||
- Determine if agents are being created new or retrieved from hosted services:
|
||||
- **New agents**: Use `CreateAIAgent()` methods
|
||||
- **Existing hosted agents**: Use `GetAIAgent(agentId)` methods for OpenAI Assistants and Azure AI Foundry
|
||||
- **Existing hosted agents**: Use `GetAIAgent(agentId)` methods for OpenAI Assistants and Microsoft Foundry
|
||||
</agent_type_identification>
|
||||
|
||||
- Determine the AI provider being used (OpenAI, Azure OpenAI, Azure AI Foundry, etc.)
|
||||
- Determine the AI provider being used (OpenAI, Azure OpenAI, Microsoft Foundry, etc.)
|
||||
- Analyze tool/function registration patterns
|
||||
- Review thread management and invocation patterns
|
||||
|
||||
@@ -90,7 +90,7 @@ below in wrong order or skip any of them):
|
||||
you generate report when migration complete. Report should contain:
|
||||
- all project dependencies changes (mention what was changed, added or removed, including provider-specific packages)
|
||||
- all code files that were changed (mention what was changed in the file, if it was not changed, just mention that the file was not changed)
|
||||
- provider-specific migration patterns used (OpenAI, Azure OpenAI, Azure AI Foundry, A2A, ONNX, etc.)
|
||||
- provider-specific migration patterns used (OpenAI, Azure OpenAI, Microsoft Foundry, A2A, ONNX, etc.)
|
||||
- all cases where you could not convert the code because of unsupported features and you were unable to find a workaround
|
||||
- unsupported providers that require custom implementation (Bedrock, CopilotStudio)
|
||||
- breaking glass pattern migrations (InnerContent → RawRepresentation) and any CodeInterpreter or advanced tool usage
|
||||
@@ -223,7 +223,7 @@ using Microsoft.Agents.AI;
|
||||
// Provider-specific namespaces (add only if needed):
|
||||
using OpenAI; // For OpenAI provider
|
||||
using Azure.AI.OpenAI; // For Azure OpenAI provider
|
||||
using Azure.AI.Agents.Persistent; // For Azure AI Foundry provider
|
||||
using Azure.AI.Agents.Persistent; // For Microsoft Foundry provider
|
||||
using Azure.Identity; // For Azure authentication
|
||||
```
|
||||
</configuration_changes>
|
||||
@@ -499,7 +499,7 @@ For every thread created if there's intent to cleanup, the caller should track a
|
||||
var assistantClient = new OpenAIClient(apiKey).GetAssistantClient();
|
||||
await assistantClient.DeleteThreadAsync(thread.ConversationId);
|
||||
|
||||
// For Azure AI Foundry (when cleanup is needed):
|
||||
// For Microsoft Foundry (when cleanup is needed):
|
||||
var persistentClient = new PersistentAgentsClient(endpoint, credential);
|
||||
await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
|
||||
|
||||
@@ -514,7 +514,7 @@ await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
|
||||
1. Remove `thread.DeleteAsync()` calls
|
||||
2. Use provider-specific client for cleanup when required
|
||||
3. Access thread ID via `thread.ConversationId` property
|
||||
4. Only implement cleanup for providers that require it (Assistants, Azure AI Foundry)
|
||||
4. Only implement cleanup for providers that require it (Assistants, Microsoft Foundry)
|
||||
</api_changes>
|
||||
|
||||
### Provider-Specific Creation Patterns
|
||||
@@ -550,13 +550,13 @@ AIAgent agent = new AzureOpenAIClient(endpoint, credential)
|
||||
.CreateAIAgent(instructions: instructions);
|
||||
```
|
||||
|
||||
**Azure AI Foundry (New):**
|
||||
**Microsoft Foundry (New):**
|
||||
```csharp
|
||||
AIAgent agent = new PersistentAgentsClient(endpoint, credential)
|
||||
.CreateAIAgent(model: deploymentName, instructions: instructions);
|
||||
```
|
||||
|
||||
**Azure AI Foundry (Existing):**
|
||||
**Microsoft Foundry (Existing):**
|
||||
```csharp
|
||||
AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
|
||||
.GetAIAgentAsync(agentId);
|
||||
@@ -1079,7 +1079,7 @@ AgentThread thread = agent.GetNewThread();
|
||||
```
|
||||
</api_changes>
|
||||
|
||||
### 4. Azure AI Foundry (AzureAIAgent) Migration
|
||||
### 4. Microsoft Foundry (AzureAIAgent) Migration
|
||||
|
||||
<configuration_changes>
|
||||
**Remove Semantic Kernel Packages:**
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -64,6 +64,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
coreChanged: ${{ steps.filter.outputs.core }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -163,6 +163,7 @@ jobs:
|
||||
# Change to project directory to ensure local nuget.config is used
|
||||
pushd consoleapp
|
||||
dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease
|
||||
dotnet add packcheck.csproj package Microsoft.Agents.AI.LocalCodeAct --prerelease
|
||||
dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj
|
||||
|
||||
# Clean up
|
||||
@@ -312,7 +313,7 @@ jobs:
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
|
||||
# Azure AI Foundry
|
||||
# Microsoft Foundry
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
|
||||
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
|
||||
@@ -528,7 +529,7 @@ jobs:
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
|
||||
# Azure AI Foundry
|
||||
# Microsoft Foundry
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
|
||||
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
|
||||
@@ -615,7 +616,7 @@ jobs:
|
||||
pattern: dotnet-test-results-*
|
||||
path: dotnet-test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: python/dotnet-integration-report-history.json
|
||||
key: dotnet-integration-report-history-${{ github.run_id }}
|
||||
@@ -632,7 +633,7 @@ jobs:
|
||||
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: python/dotnet-integration-report-history.json
|
||||
key: dotnet-integration-report-history-${{ github.run_id }}
|
||||
|
||||
@@ -9,16 +9,31 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
checkout-ref:
|
||||
description: "Git ref to checkout (e.g., refs/pull/123/head)"
|
||||
description: "Immutable commit SHA to check out"
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
AZURE_CLIENT_ID:
|
||||
required: true
|
||||
AZURE_TENANT_ID:
|
||||
required: true
|
||||
AZURE_SUBSCRIPTION_ID:
|
||||
required: true
|
||||
AZUREAI__ENDPOINT:
|
||||
required: true
|
||||
COPILOT_GITHUB_TOKEN:
|
||||
required: true
|
||||
OPENAI__APIKEY:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
dotnet-integration-tests:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
|
||||
# Azure AI Foundry
|
||||
# Microsoft Foundry
|
||||
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
|
||||
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Go to Actions → "Integration Tests (Manual)" → Run workflow → enter a PR number or branch name.
|
||||
#
|
||||
# It calls dedicated integration-only workflows (dotnet-integration-tests and python-integration-tests),
|
||||
# passing a ref so they check out and test the correct code.
|
||||
# passing an immutable commit SHA so they check out and test the approved code.
|
||||
# Changed paths are detected here so only the relevant test suites run.
|
||||
#
|
||||
|
||||
@@ -26,7 +26,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: integration-tests-manual-${{ github.event.inputs.pr-number || github.event.inputs.branch }}
|
||||
@@ -38,67 +37,50 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
checkout-ref: ${{ steps.resolve.outputs.checkout-ref }}
|
||||
base-ref: ${{ steps.resolve.outputs.base-ref }}
|
||||
dotnet-changes: ${{ steps.detect-changes.outputs.dotnet }}
|
||||
python-changes: ${{ steps.detect-changes.outputs.python }}
|
||||
steps:
|
||||
- name: Resolve checkout ref
|
||||
- name: Check out trusted workflow helpers
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/scripts
|
||||
|
||||
- name: Resolve and authorize checkout ref
|
||||
id: resolve
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const resolveIntegrationTestTarget = require(
|
||||
'./.github/scripts/resolve_integration_test_target.js'
|
||||
);
|
||||
const target = await resolveIntegrationTestTarget({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber: process.env.PR_NUMBER,
|
||||
branch: process.env.BRANCH,
|
||||
});
|
||||
core.setOutput('checkout-ref', target.checkoutRef);
|
||||
core.setOutput('base-ref', target.baseRef);
|
||||
core.info(`Running integration tests for ${target.description} at ${target.checkoutRef}.`);
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.inputs.pr-number }}
|
||||
BRANCH: ${{ github.event.inputs.branch }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$BRANCH" ]; then
|
||||
echo "::error::Please provide either a PR number or a branch name, not both."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$PR_NUMBER" ] && [ -z "$BRANCH" ]; then
|
||||
echo "::error::Please provide either a PR number or a branch name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
if ! echo "$PR_NUMBER" | grep -Eq '^[0-9]+$'; then
|
||||
echo "::error::Invalid PR number. Only numeric values are allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state)
|
||||
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
|
||||
|
||||
if [ "$PR_STATE" != "OPEN" ]; then
|
||||
echo "::error::PR #$PR_NUMBER is not open (state: $PR_STATE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "checkout-ref=refs/pull/$PR_NUMBER/head" >> "$GITHUB_OUTPUT"
|
||||
echo "Running integration tests for PR #$PR_NUMBER"
|
||||
else
|
||||
if ! echo "$BRANCH" | grep -Eq '^[a-zA-Z0-9_./-]+$'; then
|
||||
echo "::error::Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes are allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "checkout-ref=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
echo "Running integration tests for branch $BRANCH"
|
||||
fi
|
||||
|
||||
- name: Detect changed paths
|
||||
id: detect-changes
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.inputs.pr-number }}
|
||||
BRANCH: ${{ github.event.inputs.branch }}
|
||||
BASE_REF: ${{ steps.resolve.outputs.base-ref }}
|
||||
CHECKOUT_REF: ${{ steps.resolve.outputs.checkout-ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only)
|
||||
else
|
||||
# For branches, compare against main using the GitHub API
|
||||
CHANGED_FILES=$(gh api "repos/$REPO/compare/main...$BRANCH" --jq '.files[].filename')
|
||||
fi
|
||||
CHANGED_FILES=$(gh api "repos/$REPO/compare/$BASE_REF...$CHECKOUT_REF" \
|
||||
--jq '.files[].filename')
|
||||
|
||||
DOTNET_CHANGES=false
|
||||
PYTHON_CHANGES=false
|
||||
@@ -113,22 +95,41 @@ jobs:
|
||||
|
||||
echo "dotnet=$DOTNET_CHANGES" >> "$GITHUB_OUTPUT"
|
||||
echo "python=$PYTHON_CHANGES" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected changes — dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
|
||||
echo "Detected changes; dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
|
||||
|
||||
dotnet-integration-tests:
|
||||
name: .NET Integration Tests
|
||||
needs: resolve-ref
|
||||
if: needs.resolve-ref.outputs.dotnet-changes == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/dotnet-integration-tests.yml
|
||||
with:
|
||||
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
|
||||
secrets: inherit
|
||||
secrets:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZUREAI__ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
|
||||
python-integration-tests:
|
||||
name: Python Integration Tests
|
||||
needs: resolve-ref
|
||||
if: needs.resolve-ref.outputs.python-changes == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/python-integration-tests.yml
|
||||
with:
|
||||
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
|
||||
secrets: inherit
|
||||
secrets:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY }}
|
||||
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
|
||||
@@ -3,6 +3,12 @@ name: Issue Triage
|
||||
on:
|
||||
issues:
|
||||
types: [opened, typed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: Issue number to triage
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -12,7 +18,8 @@ permissions:
|
||||
concurrency:
|
||||
group: >-
|
||||
issue-triage-${{ github.repository }}-${{
|
||||
github.event.issue.type.name == 'Bug' && github.event.issue.number
|
||||
github.event_name == 'workflow_dispatch' && inputs.issue_number
|
||||
|| github.event.issue.type.name == 'Bug' && github.event.issue.number
|
||||
|| github.run_id
|
||||
}}
|
||||
cancel-in-progress: true
|
||||
@@ -26,7 +33,11 @@ env:
|
||||
jobs:
|
||||
team_check:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.type.name == 'Bug' }}
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|| github.event.issue.type.name == 'Bug'
|
||||
}}
|
||||
outputs:
|
||||
is_team_member: ${{ steps.check.outputs.is_team_member }}
|
||||
issue_number: ${{ steps.issue.outputs.issue_number }}
|
||||
@@ -36,14 +47,18 @@ jobs:
|
||||
id: issue
|
||||
shell: bash
|
||||
env:
|
||||
ISSUE_NUMBER_EVENT: ${{ github.event.issue.number }}
|
||||
ISSUE_NUMBER: >-
|
||||
${{
|
||||
github.event_name == 'workflow_dispatch' && inputs.issue_number
|
||||
|| github.event.issue.number
|
||||
}}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
issue_number="${ISSUE_NUMBER_EVENT}"
|
||||
issue_number="${ISSUE_NUMBER}"
|
||||
|
||||
if [[ ! "$issue_number" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Could not determine issue number from event payload." >&2
|
||||
echo "Could not determine issue number from event payload or manual input." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -58,6 +73,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check issue author team membership
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
id: check
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
@@ -84,7 +100,11 @@ jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: team_check
|
||||
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|| needs.team_check.outputs.is_team_member == 'false'
|
||||
}}
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
|
||||
@@ -114,7 +134,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
|
||||
@@ -13,13 +13,27 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
checkout-ref:
|
||||
description: "Git ref to checkout (e.g., refs/pull/123/head)"
|
||||
description: "Immutable commit SHA to check out"
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
ANTHROPIC_API_KEY:
|
||||
required: true
|
||||
AZURE_CLIENT_ID:
|
||||
required: true
|
||||
AZURE_TENANT_ID:
|
||||
required: true
|
||||
AZURE_SUBSCRIPTION_ID:
|
||||
required: true
|
||||
COPILOT_GITHUB_TOKEN:
|
||||
required: true
|
||||
FOUNDRY_MODELS_API_KEY:
|
||||
required: false
|
||||
OPENAI__APIKEY:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
@@ -99,6 +113,9 @@ jobs:
|
||||
# Azure OpenAI integration tests
|
||||
python-tests-azure-openai:
|
||||
name: Python Integration Tests - Azure OpenAI
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
@@ -224,6 +241,7 @@ jobs:
|
||||
packages/hyperlight/tests
|
||||
packages/ollama/tests
|
||||
packages/core/tests/core/test_mcp.py
|
||||
packages/hosting-mcp/tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
@@ -260,6 +278,9 @@ jobs:
|
||||
# Azure Functions + Durable Task integration tests
|
||||
python-tests-functions:
|
||||
name: Python Integration Tests - Functions
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
@@ -324,6 +345,9 @@ jobs:
|
||||
# Foundry integration tests
|
||||
python-tests-foundry:
|
||||
name: Python Integration Tests - Foundry
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
@@ -378,6 +402,9 @@ jobs:
|
||||
# Foundry Hosting integration tests
|
||||
python-tests-foundry-hosting:
|
||||
name: Python Integration Tests - Foundry Hosting
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
@@ -551,7 +578,7 @@ jobs:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-integration-${{ github.run_id }}
|
||||
@@ -568,7 +595,7 @@ jobs:
|
||||
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-integration-${{ github.run_id }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
|
||||
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -71,6 +71,7 @@ jobs:
|
||||
- 'python/packages/ollama/**'
|
||||
- 'python/packages/core/agent_framework/_mcp.py'
|
||||
- 'python/packages/core/tests/core/test_mcp.py'
|
||||
- 'python/packages/hosting-mcp/**'
|
||||
- 'python/scripts/local_mcp_streamable_http_server.py'
|
||||
- '.github/actions/setup-local-mcp-server/**'
|
||||
- '.github/workflows/python-merge-tests.yml'
|
||||
@@ -345,6 +346,7 @@ jobs:
|
||||
packages/hyperlight/tests
|
||||
packages/ollama/tests
|
||||
packages/core/tests/core/test_mcp.py
|
||||
packages/hosting-mcp/tests
|
||||
-m integration
|
||||
-n logical --dist worksteal
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
@@ -748,7 +750,7 @@ jobs:
|
||||
pattern: test-results-*
|
||||
path: test-results/
|
||||
- name: Restore report history cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-merge-${{ github.run_id }}
|
||||
@@ -765,7 +767,7 @@ jobs:
|
||||
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
|
||||
- name: Save report history cache
|
||||
if: always()
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: python/integration-report-history.json
|
||||
key: integration-report-history-merge-${{ github.run_id }}
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Build the package
|
||||
run: uv run poe --directory packages/${{ env.PACKAGE }} build
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
with:
|
||||
files: |
|
||||
python/dist/*
|
||||
|
||||
@@ -701,7 +701,7 @@ jobs:
|
||||
|
||||
- name: Restore validation history
|
||||
id: cache-restore
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
@@ -719,7 +719,7 @@ jobs:
|
||||
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save validation history
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: validation-history/
|
||||
key: validation-history-${{ github.run_id }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
|
||||
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
**What is Microsoft Agent Framework?**
|
||||
|
||||
Microsoft Agent Framework is a comprehensive multi-language (C#/.NET and Python) framework for building, orchestrating, and deploying AI agents and multi-agent workflows. The system takes user instructions and conversation inputs and produces intelligent responses through AI agents that can integrate with various LLM providers (OpenAI, Azure OpenAI, Azure AI Foundry). It provides both simple chat agents and complex multi-agent workflows with graph-based orchestration.
|
||||
Microsoft Agent Framework is a comprehensive multi-language (C#/.NET and Python) framework for building, orchestrating, and deploying AI agents and multi-agent workflows. The system takes user instructions and conversation inputs and produces intelligent responses through AI agents that can integrate with various LLM providers (OpenAI, Azure OpenAI, Microsoft Foundry). It provides both simple chat agents and complex multi-agent workflows with graph-based orchestration.
|
||||
|
||||
**What can Microsoft Agent Framework do?**
|
||||
|
||||
@@ -12,7 +12,7 @@ The framework offers:
|
||||
- **Multi-Agent Orchestration**: Group chat, sequential, concurrent, and handoff patterns
|
||||
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, time-travel, and Human-in-the-loop
|
||||
- **Extensibility Framework**: Extend with native functions, A2A, Model Context Protocol (MCP)
|
||||
- **LLM Integration**: Support for OpenAI, Azure OpenAI, Azure AI Foundry, and other providers
|
||||
- **LLM Integration**: Support for OpenAI, Azure OpenAI, Microsoft Foundry, and other providers
|
||||
- **Runtime Support**: Both in-process and distributed agent execution
|
||||
|
||||
**What is/are Microsoft Agent Framework's intended use(s)?**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2025-07-10 {YYYY-MM-DD when the decision was last updated}
|
||||
date: 2025-07-10
|
||||
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
|
||||
consulted:
|
||||
informed:
|
||||
@@ -139,7 +139,7 @@ Therefore something like `AgentResponse.Text` which also aggregates all `TextCon
|
||||
|
||||
#### Option 1.2 Presence of Secondary Content is determined by a runtime parameter
|
||||
|
||||
We can allow callers to choose whether to include secondary content in the list of reponse messages.
|
||||
We can allow callers to choose whether to include secondary content in the list of response messages.
|
||||
Open Question: Do we allow secondary content to use `TextContent` types?
|
||||
|
||||
```csharp
|
||||
|
||||
@@ -113,7 +113,7 @@ Implement a hybrid strategy where common tools use generic `AITool`-derived abst
|
||||
|
||||
### AI Agent Tool Types Availability
|
||||
|
||||
Tool Type | Azure AI Foundry Agent Service | OpenAI Assistant API | OpenAI ChatCompletion API | OpenAI Responses API | Amazon Bedrock Agents | Google | Anthropic | Description
|
||||
Tool Type | Microsoft Foundry Agent Service | OpenAI Assistant API | OpenAI ChatCompletion API | OpenAI Responses API | Amazon Bedrock Agents | Google | Anthropic | Description
|
||||
-- | -- | -- | -- | -- | -- | -- | -- | --
|
||||
Function Calling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Enables custom, stateless functions to define specific agent behaviors.
|
||||
Code Interpreter | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | Allows agents to execute code for tasks like data analysis or problem-solving.
|
||||
@@ -132,7 +132,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### Function Calling
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest</a>
|
||||
|
||||
Message Request:
|
||||
@@ -401,7 +401,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### Code Interpreter
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
<p>Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api</a></p>
|
||||
|
||||
<p>.NET Support: ✅</p>
|
||||
@@ -709,7 +709,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### Search and Retrieval
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest</a>
|
||||
|
||||
File Search Request:
|
||||
@@ -1083,7 +1083,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### Web Search
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest</a>
|
||||
|
||||
Bing Search Message Request:
|
||||
@@ -1630,7 +1630,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### OpenAPI Spec Tool
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api</a><br>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall">https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall</a>
|
||||
|
||||
@@ -1712,7 +1712,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### Stateful Functions
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest</a>
|
||||
|
||||
Message Request:
|
||||
@@ -1832,7 +1832,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
|
||||
|
||||
#### Microsoft Fabric
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agent Service</summary>
|
||||
<summary>Microsoft Foundry Agent Service</summary>
|
||||
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest</a>
|
||||
|
||||
Message Request:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: accepted
|
||||
contact: westey-m
|
||||
date: 2025-09-12 {YYYY-MM-DD when the decision was last updated}
|
||||
date: 2025-09-12
|
||||
deciders: sergeymenshykh, markwallace-microsoft, rogerbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, peterychang
|
||||
consulted:
|
||||
informed:
|
||||
@@ -25,7 +25,7 @@ See various features that would need to be supported via this type of mechanism,
|
||||
- Also see [the openai human-in-the-loop guide](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/#approval-requests).
|
||||
- Also see [the openai MCP guide](https://openai.github.io/openai-agents-js/guides/mcp/#optional-approval-flow).
|
||||
- Also see [MCP Approval Requests from OpenAI](https://platform.openai.com/docs/guides/tools-remote-mcp#approvals).
|
||||
- Also see [Azure AI Foundry MCP Approvals](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/model-context-protocol-samples?pivots=rest#submit-your-approval).
|
||||
- Also see [Microsoft Foundry MCP Approvals](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/model-context-protocol-samples?pivots=rest#submit-your-approval).
|
||||
- Also see [MCP Elicitation requests](https://modelcontextprotocol.io/specification/draft/client/elicitation)
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
@@ -57,7 +57,7 @@ This section describes different options for various aspects required to add lon
|
||||
|
||||
### 1. Methods for Working with Long-Running Operations
|
||||
|
||||
Based on the analysis of existing APIs that support long-running operations (such as OpenAI Responses, Azure AI Foundry Agents, and A2A),
|
||||
Based on the analysis of existing APIs that support long-running operations (such as OpenAI Responses, Microsoft Foundry Agents, and A2A),
|
||||
the following operations are used for working with long-running operations:
|
||||
- Common operations:
|
||||
- **Start Long-Running Execution**: Initiates a long-running operation and returns its Id.
|
||||
@@ -757,7 +757,7 @@ Some of them natively support resuming streaming from a specific point in the st
|
||||
| API | Can Resume Streaming | Model |
|
||||
|-------------------------|--------------------------------------|------------------------------------------------------------------------------------------------------------|
|
||||
| OpenAI Responses | Yes | StreamingResponseUpdate.**SequenceNumber** + GetResponseStreamingAsync(responseId, **startingAfter**, ct) |
|
||||
| Azure AI Foundry Agents | Emulated<sup>2</sup> | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
|
||||
| Microsoft Foundry Agents | Emulated<sup>2</sup> | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
|
||||
| A2A | Implementation dependent<sup>1</sup> | |
|
||||
|
||||
<sup>1</sup> The [A2A specification](https://github.com/a2aproject/A2A/blob/main/docs/topics/streaming-and-async.md#1-streaming-with-server-sent-events-sse)
|
||||
@@ -765,7 +765,7 @@ allows an A2A agent implementation to decide how to handle streaming resumption:
|
||||
a task is still active (and the server hasn't sent a final: true event for that phase), the client can attempt to reconnect to the stream using the tasks/resubscribe RPC method.
|
||||
The server's behavior regarding missed events during the disconnection period (e.g., whether it backfills or only sends new updates) is implementation-dependent._
|
||||
|
||||
<sup>2</sup> The Azure AI Foundry Agents API has an API to start a streaming run but does not have an API to resume streaming from a specific point in the stream.
|
||||
<sup>2</sup> The Microsoft Foundry Agents API has an API to start a streaming run but does not have an API to resume streaming from a specific point in the stream.
|
||||
However, it has non-streaming APIs to access already started runs, which can be used to emulate streaming resumption by accessing a run and its steps and streaming all the steps after a specific step.
|
||||
|
||||
#### Required Changes
|
||||
@@ -828,7 +828,7 @@ Sequence of updates from OpenAI Responses API to answer the question "What time
|
||||
| resp_2 | 10 | resp.output_item.done | - | InProgress | |
|
||||
| resp_2 | 11 | resp.completed | Completed | Completed | |
|
||||
|
||||
Sequence of updates from Azure AI Foundry Agents API to answer the question "What time is it?" using a function call:
|
||||
Sequence of updates from Microsoft Foundry Agents API to answer the question "What time is it?" using a function call:
|
||||
| Id | SN | UpdateKind | Run.Status | Step.Status | Message.Status | ChatResponseUpdate.Status | Description |
|
||||
|--------|---------|-------------------|----------------|-------------|-----------------|---------------------------|---------------------------------------------------|
|
||||
| run_1 | - | RunCreated | Queued | - | - | Queued | |
|
||||
@@ -852,7 +852,7 @@ Sequence of updates from Azure AI Foundry Agents API to answer the question "Wha
|
||||
|
||||
To support long-running operations, the following values need to be returned by the GetResponseAsync and GetStreamingResponseAsync methods:
|
||||
- `ResponseId` - identifier of the long-running operation or an entity representing it, such as a task.
|
||||
- `ConversationId` - identifier of the conversation or thread the long-running operation is part of. Some APIs, like Azure AI Foundry Agents, use
|
||||
- `ConversationId` - identifier of the conversation or thread the long-running operation is part of. Some APIs, like Microsoft Foundry Agents, use
|
||||
this identifier together with the ResponseId to identify a run.
|
||||
- `SequenceNumber` - identifier of an update within a stream of updates. This is required to support streaming resumption by the GetStreamingResponseAsync method only.
|
||||
- `Status` - status of the long-running operation: whether it is queued, running, failed, cancelled, completed, etc.
|
||||
@@ -1089,7 +1089,7 @@ public class ChatOptions
|
||||
|
||||
##### 6.1.5 Continuation Token of a Custom Type
|
||||
|
||||
The option is similar the the "6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type" option but suggests using a
|
||||
The option is similar to the "6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type" option but suggests using a
|
||||
custom type for the continuation token instead of the `System.ClientModel.ContinuationToken` type.
|
||||
|
||||
**Pros**
|
||||
@@ -1203,7 +1203,7 @@ response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOpt
|
||||
In case an agent supports either or both cancellation and deletion of long-running operations, it will override the corresponding methods.
|
||||
Otherwise, it won't override them, and the base implementations will return null by default.
|
||||
|
||||
Some agents, for example Azure AI Foundry Agents, require the thread identifier to cancel a run. To accommodate this requirement, the `CancelRunAsync` method
|
||||
Some agents, for example Microsoft Foundry Agents, require the thread identifier to cancel a run. To accommodate this requirement, the `CancelRunAsync` method
|
||||
accepts an optional `AgentCancelRunOptions` parameter that allows callers to specify the thread associated with the run they want to cancel.
|
||||
|
||||
```csharp
|
||||
@@ -1574,7 +1574,7 @@ the thread is provided with background operations consistently for all runs.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Azure AI Foundry Agents</summary>
|
||||
<summary>Microsoft Foundry Agents</summary>
|
||||
|
||||
- Create a thread and run the agent against it and wait for it to complete using polling:
|
||||
```csharp
|
||||
|
||||
@@ -34,11 +34,11 @@ Key changes:
|
||||
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
|
||||
2. **Class renames**: `OpenAIResponsesClient` → `OpenAIChatClient` (Responses API), `OpenAIChatClient` → `OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
|
||||
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
|
||||
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
|
||||
4. **New `FoundryChatClient`** in azure-ai for Microsoft Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
|
||||
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
|
||||
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
|
||||
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
|
||||
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
|
||||
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Microsoft Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
|
||||
|
||||
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ consulted: Pratyush Mishra, Shivam Shrivastava, Manni Arora (Centrica eval scena
|
||||
informed: Agent Framework team, Foundry Evals team
|
||||
---
|
||||
|
||||
# Agent Evaluation Architecture with Azure AI Foundry Integration
|
||||
# Agent Evaluation Architecture with Microsoft Foundry Integration
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
Azure AI Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
|
||||
Microsoft Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
|
||||
|
||||
However, using Foundry Evals with an agent-framework agent today requires significant manual effort. Developers must:
|
||||
|
||||
@@ -445,7 +445,7 @@ These factorings produce different scores for the same conversation. The framewo
|
||||
|
||||
### Azure AI: FoundryEvals
|
||||
|
||||
`Evaluator` implementation backed by Azure AI Foundry:
|
||||
`Evaluator` implementation backed by Microsoft Foundry:
|
||||
|
||||
```python
|
||||
class FoundryEvals:
|
||||
@@ -812,4 +812,4 @@ public sealed class EvalItem
|
||||
|
||||
## More Information
|
||||
|
||||
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Azure AI Foundry evaluation overview
|
||||
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Microsoft Foundry evaluation overview
|
||||
|
||||
@@ -9,7 +9,7 @@ deciders: evmattso
|
||||
|
||||
## What is the goal of this feature?
|
||||
|
||||
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in an Azure AI Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
|
||||
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in a Microsoft Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
|
||||
|
||||
A user who has configured a toolbox in the Foundry portal (or via the raw SDK) should be able to load it into an agent with a single call:
|
||||
|
||||
|
||||
@@ -181,8 +181,8 @@ The app chooses which helper to call for that route and deployment. For example:
|
||||
|
||||
- `responses_session_id(body)` from `agent-framework-hosting-responses`, which can return either a `resp_*` previous
|
||||
response id or a `conv_*` conversation id when present;
|
||||
- `telegram_session_id(update)` from `agent-framework-hosting-telegram`, which can choose the chat, user, thread, or
|
||||
other Telegram-native partitioning logic for that helper;
|
||||
- `telegram_session_id(update, bot_id=...)` from `agent-framework-hosting-telegram`, which uses the bot and sender for
|
||||
private chats and the bot and chat for shared group sessions;
|
||||
- `activity_session_id(activity)`, `discord_session_id(interaction_or_message)`, or
|
||||
`a2a_session_id(request_context)` from their respective protocol packages;
|
||||
- `foundry_user_isolation_key()` or `foundry_chat_isolation_key()` from `agent-framework-foundry-hosting`.
|
||||
@@ -204,7 +204,8 @@ safe to use:
|
||||
transient execution.
|
||||
|
||||
A `SessionStore` stores `session_id -> AgentSession`, but it does not create sessions. `AgentState` resolves the agent
|
||||
target and creates the session on first use:
|
||||
target and creates the session on first use. Reads return independent working copies so running from one continuation
|
||||
point does not mutate the stored snapshot or another simultaneous branch:
|
||||
|
||||
For agent targets:
|
||||
|
||||
@@ -227,6 +228,11 @@ await state.set_session(response_id, session)
|
||||
`agent.run(...)` may update the session object (for example, with service continuation state), so the explicit store call
|
||||
belongs after the run, not before it.
|
||||
|
||||
Response ids are immutable continuation points, so simultaneous callers can branch from one `previous_response_id` and
|
||||
store their completed sessions under different new response ids. A stable `conversation_id` is a mutable head: the app
|
||||
must explicitly update it after the run and provide single-writer coordination. The hosting state helper does not lock
|
||||
an entire run or resolve concurrent updates to that stable key.
|
||||
|
||||
The session id is a partition key, not proof of identity. App or platform code must authenticate and authorize any
|
||||
externally supplied key before using it.
|
||||
|
||||
|
||||
@@ -66,8 +66,11 @@ must be aligned with the helper-first model before implementation. Old vocabular
|
||||
| Package | Import surface | v1 helper-first contents |
|
||||
|---|---|---|
|
||||
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
|
||||
| `agent-framework-hosting-a2a` | `agent_framework_hosting_a2a` | A2A `Message` to run conversion and Agent Framework output to A2A `Part` conversion. |
|
||||
| `agent-framework-hosting-mcp` | `agent_framework_hosting_mcp` | Agent and workflow MCP tool adapters, MCP tool arguments to run conversion, and Agent Framework output to MCP `ContentBlock` conversion. |
|
||||
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
|
||||
| Future protocol packages | e.g. `agent_framework_hosting_telegram` | Protocol-specific helpers such as `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_session_id(...)`, and command/media helpers when useful. |
|
||||
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | Telegram Bot API helpers: update parsing, chat/session/command/media extraction, final rendering, and streaming edit rendering. |
|
||||
| Future protocol packages | e.g. `agent_framework_hosting_activity_protocol` | Protocol-specific helpers such as `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, and command/media helpers when useful. |
|
||||
|
||||
The core hosting package must not depend on protocol SDKs. Protocol packages may depend on their native protocol SDKs if
|
||||
needed, but helper functions should stay usable from plain app code and tests.
|
||||
@@ -90,6 +93,7 @@ Examples:
|
||||
|
||||
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
|
||||
`responses_session_id(...)`;
|
||||
- `a2a_to_run(...)`, `a2a_from_run(...)`;
|
||||
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
|
||||
`telegram_session_id(...)`, `telegram_command(...)`;
|
||||
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
|
||||
@@ -177,6 +181,9 @@ The target may be:
|
||||
- `await get_target()`;
|
||||
- synchronous `target` only after a target is already available/resolved.
|
||||
|
||||
A workflow instance permits one active run. Concurrent hosts use a factory or
|
||||
builder with `cache_target=False` to resolve a fresh instance per run.
|
||||
|
||||
Workflow checkpointing uses Agent Framework's existing `CheckpointStorage` abstraction directly. Apps that need
|
||||
per-session workflow resume should keep an app-owned cursor such as `session_id -> checkpoint_id`. When the app uses
|
||||
file-backed cursor storage, the file-based checkpoint storage should share the same app storage root and should be
|
||||
@@ -244,6 +251,146 @@ text deltas, and a completed event. The final completed payload is produced thro
|
||||
also preserves the model id observed on streaming updates when the finalized `AgentResponse` no longer carries raw model
|
||||
metadata.
|
||||
|
||||
## `agent-framework-hosting-a2a`
|
||||
|
||||
The A2A package provides only the conversion seam between the native A2A SDK
|
||||
and Agent Framework:
|
||||
|
||||
- `a2a_to_run(message, *, stream=False) -> AgentRunArgs`
|
||||
- `a2a_from_run(result) -> list[a2a.types.Part]`
|
||||
|
||||
`a2a_to_run(...)` accepts a native A2A `Message` and converts its text, URL,
|
||||
raw-byte, and structured-data parts into one Agent Framework user message.
|
||||
|
||||
`a2a_from_run(...)` accepts an `AgentResponse`, `Message`, or
|
||||
`AgentResponseUpdate` and converts supported text, URI, and data content into
|
||||
native A2A `Part` values. This one helper is usable for both completed and
|
||||
streaming runs.
|
||||
|
||||
The package does not provide an A2A `AgentExecutor`, application, route,
|
||||
request handler, task store, event queue, `TaskUpdater`, task-state policy,
|
||||
artifact-id policy, or session-key policy. Application code composes the two
|
||||
helpers with those native A2A SDK constructs and may use any server framework
|
||||
supported by the SDK.
|
||||
|
||||
## `agent-framework-hosting-mcp`
|
||||
|
||||
The MCP package provides only the conversion seam between native MCP SDK values
|
||||
and Agent Framework:
|
||||
|
||||
- `MCPAgentTool(target, ...)`
|
||||
- `MCPWorkflowTool(target, ...)`
|
||||
- `mcp_to_run(arguments, *, argument_name="task", chat_option_arguments=()) -> AgentRunArgs`
|
||||
- `mcp_from_run(result) -> list[mcp.types.ContentBlock]`
|
||||
|
||||
`MCPAgentTool` represents one Agent Framework agent as one native MCP tool. It
|
||||
derives the default tool name and description from the agent, accepts
|
||||
overrides for those values and the main text parameter, includes app-owned
|
||||
additional parameter schemas, and explicitly maps selected parameter schemas
|
||||
to ChatOptions. Its asynchronous `list_tools()` returns the native `Tool` list,
|
||||
and `call_tool(...)` performs conversion, agent execution, and final result
|
||||
conversion.
|
||||
|
||||
The adapter accepts either an agent or an existing `AgentState`. With a
|
||||
configured `session_id_parameter`, it loads and stores the corresponding
|
||||
`AgentSession`. The application remains responsible for deriving and
|
||||
authorizing the session id and preventing concurrent updates to the same
|
||||
session.
|
||||
|
||||
`MCPWorkflowTool` represents one Agent Framework workflow as one native MCP
|
||||
tool. It derives the tool name and description from the workflow and derives
|
||||
the input schema from the start executor's single declared input type.
|
||||
Object-shaped dataclass and Pydantic inputs become top-level MCP arguments;
|
||||
primitive inputs are wrapped in one configurable argument. The adapter
|
||||
validates the arguments against that type, runs the workflow, and converts
|
||||
terminal outputs to MCP content blocks.
|
||||
|
||||
Workflow instances preserve state and reject concurrent runs. Applications
|
||||
that need independent calls should provide a `WorkflowState` factory with
|
||||
`cache_target=False`. Checkpoint restoration, human-in-the-loop responses, and
|
||||
continuation identifiers remain application-owned contracts. If a workflow
|
||||
stops to request external input, the adapter raises rather than returning an
|
||||
empty successful tool result.
|
||||
|
||||
`mcp_to_run(...)` accepts the argument mapping from a native MCP `call_tool`
|
||||
handler. The application owns the tool schema and may select which required
|
||||
string argument contains the user request. The application should define that
|
||||
argument name once and use the same value in the native tool schema and the
|
||||
`argument_name` parameter so those two sides of the contract remain aligned.
|
||||
Applications may also expose selected ChatOptions fields in their native tool
|
||||
schema and pass those names through `chat_option_arguments`. Only explicitly
|
||||
selected names are copied to run options; the helper does not forward all MCP
|
||||
arguments or own their JSON Schema validation.
|
||||
|
||||
MCP `tools/call` arguments are JSON-only and do not have a native multimodal
|
||||
content-block union. The package does not impose a non-standard JSON
|
||||
representation for multimodal tool arguments.
|
||||
|
||||
`mcp_from_run(...)` accepts an `AgentResponse` or `Message`. It converts text,
|
||||
URI, image data, audio data, and other binary data into native MCP content
|
||||
blocks.
|
||||
|
||||
Its output is specifically the content union accepted by `CallToolResult`.
|
||||
Sampling-only values such as `ToolUseContent` belong to the separate MCP
|
||||
sampling response path and are not emitted by this hosting helper.
|
||||
|
||||
MCP `tools/call` returns one final `CallToolResult`. Streamable HTTP can carry
|
||||
multiple MCP messages and progress notifications can report operation status,
|
||||
but the protocol does not define partial tool-result content chunks.
|
||||
Experimental MCP tasks defer retrieval of the same final result. Therefore the
|
||||
conversion helpers do not expose Agent Framework streaming updates.
|
||||
|
||||
The package does not provide an MCP `Server`, handler registration, transport, route,
|
||||
session policy, authentication, authorization, or deployment wrapper.
|
||||
Application code composes the adapters and conversion helpers with native MCP SDK constructs and
|
||||
may use stdio, streamable HTTP, or another transport supported by the SDK.
|
||||
|
||||
## `agent-framework-hosting-telegram`
|
||||
|
||||
The Telegram package provides side-effect-free helpers around Telegram Bot API
|
||||
update and method payloads. It does not provide a Bot API client, polling loop,
|
||||
webhook route, command registry, retry policy, or rate limiter.
|
||||
|
||||
### Update helpers
|
||||
|
||||
- `telegram_to_run(update, *, resolve_file_url=None, stream=False) -> AgentRunArgs`
|
||||
- `telegram_chat_id(update) -> int | None`
|
||||
- `telegram_session_id(update, *, bot_id) -> str | None`
|
||||
- `telegram_command(update) -> str | None`
|
||||
- `telegram_callback_query_id(update) -> str | None`
|
||||
- `telegram_media_file_id(update_or_message) -> tuple[str, str] | None`
|
||||
|
||||
`telegram_to_run(...)` handles `message`, `edited_message`, and
|
||||
`callback_query` updates. Text and captions become AF text content. When the
|
||||
app supplies an async `resolve_file_url` callback, supported Telegram media
|
||||
file ids can become AF URI content. The package does not call Telegram's
|
||||
`getFile` method itself.
|
||||
|
||||
`telegram_session_id(..., bot_id=...)` includes the bot identity in every key.
|
||||
Private chats return `telegram:<bot_id>:<user_id>`; other chats return
|
||||
`telegram:<bot_id>:<chat_id>`, giving groups a shared session by default. This
|
||||
matches Telegram's native isolation boundaries while preventing two bots from
|
||||
sharing state accidentally. Apps that want per-user sessions inside a group
|
||||
can construct a key that includes both chat and sender ids. The app must
|
||||
authorize those Telegram identities before loading session state.
|
||||
|
||||
`telegram_command(...)` parses Telegram's `/name` and `/name@bot` syntax. It
|
||||
does not register commands or invoke handlers.
|
||||
|
||||
### Response helpers
|
||||
|
||||
- `telegram_from_run(result, *, chat_id, parse_mode=None)`
|
||||
- `telegram_from_streaming_run(stream, *, chat_id, message_id, initial_text=None, parse_mode=None)`
|
||||
|
||||
The helpers produce Telegram method/payload values for app-owned Bot API
|
||||
calls. Final rendering supports text and image URI output and applies
|
||||
Telegram's text-length boundary. Streaming rendering produces cumulative
|
||||
`editMessageText` payloads for a placeholder message id supplied by the app,
|
||||
omitting edits that match an optional `initial_text`, then renders the final
|
||||
rich output. Image-only responses remove the placeholder with `deleteMessage`
|
||||
before sending the image. The app owns the initial placeholder send, Bot API
|
||||
calls, edit throttling, retries, and failure policy.
|
||||
|
||||
## Security responsibilities
|
||||
|
||||
Protocol helper packages parse and render. They do not authenticate callers, authorize access to state, or decide which
|
||||
@@ -346,3 +493,6 @@ Implementation validation must cover:
|
||||
- Responses streaming SSE rendering;
|
||||
- HTTP round-trip tests showing a native FastAPI route using `AgentState` and Responses helpers;
|
||||
- sample type checking for the local Responses sample.
|
||||
- Telegram update parsing, chat/session/command/media extraction, final
|
||||
rendering, and streaming edit rendering;
|
||||
- sample type checking for the local Telegram polling and webhook entry points.
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.31.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.6.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.35.1" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.7.1" />
|
||||
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.Inference" Version="13.1.0-preview.1.25616.3" />
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory/AgentWithMemory_Step05_BoundedChatHistory.csproj" />
|
||||
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
|
||||
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.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.Valkey\\Microsoft.Agents.AI.Valkey.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",
|
||||
|
||||
@@ -427,6 +427,15 @@ internal static class AgentsSamples
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "AgentWithMemory_Step06_MemoryUsingAgentMemory",
|
||||
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_API_KEY", "FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "NEO4J_URI", "NEO4J_USER", "NEO4J_PASSWORD"],
|
||||
SkipReason = "Requires a running Neo4j instance; standalone sample outside the repo's CPM build.",
|
||||
},
|
||||
|
||||
// ── AgentWithRAG ────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
@@ -837,7 +846,7 @@ internal static class AgentsSamples
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step15_DeepResearch",
|
||||
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "AZURE_AI_BING_CONNECTION_ID"],
|
||||
OptionalEnvironmentVariables = ["AZURE_AI_REASONING_DEPLOYMENT_NAME"],
|
||||
SkipReason = "Requires Azure AI Foundry project with Bing search connection.",
|
||||
SkipReason = "Requires Microsoft Foundry project with Bing search connection.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// Pre-build the solution before running, or pass --build to avoid missing build output failures.
|
||||
//
|
||||
// Required environment variables (for AI-powered verification):
|
||||
// FOUNDRY_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint
|
||||
// FOUNDRY_PROJECT_ENDPOINT — Your Microsoft Foundry project endpoint
|
||||
// FOUNDRY_MODEL — Model deployment name (optional, defaults to gpt-5.4-mini)
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -130,7 +130,7 @@ internal static class WorkflowSamples
|
||||
ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
SkipReason = "Requires Azure AI Foundry project endpoint.",
|
||||
SkipReason = "Requires Microsoft Foundry project endpoint.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.13.0</VersionPrefix>
|
||||
<VersionPrefix>1.14.0</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<DateSuffix>260703</DateSuffix>
|
||||
<DateSuffix>260721</DateSuffix>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.13.0</GitTag>
|
||||
<GitTag>1.14.0</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -66,6 +66,18 @@ When using multiple providers (e.g., skills + file access), combine their rules
|
||||
})
|
||||
```
|
||||
|
||||
## ⚠️ Security: avoid tool-name collisions
|
||||
|
||||
Built-in auto-approval rules match tool calls **solely by tool name**. A rule cannot tell the
|
||||
provider's own tool apart from any other registered tool that happens to share the same name. If a
|
||||
different tool — especially one with a caller-configurable name, such as the Harness shell tool
|
||||
(`HarnessAgentOptions.ShellToolName`) — is registered under a name that one of these rules approves
|
||||
(e.g. `load_skill`, `read_skill_resource`, `run_skill_script`, or the `file_access_*` names), that
|
||||
tool will be **silently auto-approved**, bypassing the human approval boundary.
|
||||
|
||||
When using auto-approval rules, ensure no other tool's name collides with the reserved names the
|
||||
rules approve, and never assign a configurable tool name that matches one of them.
|
||||
|
||||
## Skills Included
|
||||
|
||||
### unit-converter
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
This project is part of the repo's solution and targets .NET 10 like the rest of the repo, but it
|
||||
intentionally opts out of Central Package Management and source-referencing Microsoft.Agents.AI:
|
||||
it consumes the *published* AgentMemory NuGet packages (which target Microsoft.Agents.AI 1.9.0)
|
||||
instead. Run it with `dotnet run` from this folder.
|
||||
|
||||
ManagePackageVersionsCentrally is off, but dotnet/Directory.Packages.props still unconditionally
|
||||
merges its repo-wide analyzer PackageReference items (no Version, resolved via CPM) into every
|
||||
project that imports it — including this one. With CPM off here those versions can't resolve
|
||||
(NU1015), so each is removed and re-added with an explicit version below (matching
|
||||
AgentWithRAG_Step05_Neo4jGraphRAG, which hits the same issue). xunit.analyzers/Moq.Analyzers are
|
||||
dropped rather than re-added since this project has no test code.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
<RootNamespace>AgentMemoryShoppingAssistant</RootNamespace>
|
||||
<!-- OPENAI001: the OpenAIClient(AuthenticationPolicy, options) ctor used for keyless Azure auth is
|
||||
marked experimental in the OpenAI SDK (the MAF Foundry samples use the same pattern). -->
|
||||
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- AgentMemory (published) — an unofficial .NET port of the Neo4j Labs agent-memory library + its
|
||||
Microsoft Agent Framework adapter. -->
|
||||
<PackageReference Include="AgentMemory" Version="1.2.0" />
|
||||
<PackageReference Include="AgentMemory.AgentFramework" Version="1.2.0" />
|
||||
<!-- Microsoft Agent Framework (matches AgentMemory's target) + the OpenAI/Foundry chat & embedding clients. -->
|
||||
<PackageReference Include="Microsoft.Agents.AI" Version="1.9.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.21.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.17" />
|
||||
<!-- Transitive dependency of Microsoft.Agents.AI; pinned explicitly (CPM is off here) because the
|
||||
version it would otherwise resolve to, 1.12.0, has a known moderate severity vulnerability
|
||||
(GHSA-g94r-2vxg-569j) that fails the repo's NuGet audit (NU1902 as error). Matches the version
|
||||
pinned in dotnet/Directory.Packages.props. -->
|
||||
<PackageReference Include="OpenTelemetry.Api" Version="1.15.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using AgentMemory.Neo4j.Infrastructure;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Neo4j.Driver;
|
||||
|
||||
namespace AgentMemoryShoppingAssistant;
|
||||
|
||||
/// <summary>
|
||||
/// A small retail product graph plus the shopping tools that query it — the .NET counterpart of the
|
||||
/// Python retail-assistant's <c>get_product_tools</c>. Products live in Neo4j as <c>:Product</c> nodes
|
||||
/// linked to <c>:ProductCategory</c> / <c>:ProductBrand</c> nodes, so recommendations and "related
|
||||
/// products" come from graph traversals. Cypher runs through the public <see cref="INeo4jTransactionRunner"/>
|
||||
/// seam. Exposed as <see cref="AIFunction"/>s so a real chat model can call them during a run — the same
|
||||
/// way <c>Neo4jMemoryContextProvider</c> surfaces the memory tools through <c>AIContext.Tools</c> when
|
||||
/// <c>ExposeMemoryToolsFromContextProvider</c> is enabled.
|
||||
/// </summary>
|
||||
public sealed class ProductCatalog(INeo4jTransactionRunner runner)
|
||||
{
|
||||
private readonly INeo4jTransactionRunner _runner = runner;
|
||||
|
||||
private static readonly (string Name, string Category, string Brand, double Price, bool InStock, int Inventory, string Description, int Popularity)[] s_seed =
|
||||
[
|
||||
("Nike Air Zoom Pegasus 40", "shoes", "Nike", 130, true, 40, "Everyday running shoe with responsive cushioning.", 95),
|
||||
("Nike Revolution 7", "shoes", "Nike", 70, true, 60, "Lightweight, budget-friendly running shoe.", 80),
|
||||
("Adidas Ultraboost Light", "shoes", "Adidas", 190, true, 25, "Premium running shoe with Boost cushioning.", 90),
|
||||
("Asics Gel-Kayano 31", "shoes", "Asics", 165, false, 0, "Stability running shoe for overpronation.", 70),
|
||||
("Sony WH-1000XM5", "electronics", "Sony", 350, true, 18, "Industry-leading noise-cancelling headphones.", 92),
|
||||
("Bose QuietComfort Ultra", "electronics", "Bose", 330, true, 12, "Premium noise-cancelling over-ear headphones.", 85),
|
||||
("Apple AirPods Pro 2", "electronics", "Apple", 250, true, 50, "Wireless earbuds with active noise cancellation.", 88),
|
||||
("Garmin Forerunner 265", "electronics", "Garmin", 450, true, 9, "GPS running watch with training metrics.", 78),
|
||||
("Nike Dri-FIT Running Tee", "apparel", "Nike", 35, true, 120, "Breathable, moisture-wicking running shirt.", 65),
|
||||
("Adidas Own the Run Jacket","apparel", "Adidas", 80, true, 33, "Lightweight, water-repellent running jacket.", 60),
|
||||
];
|
||||
|
||||
/// <summary>Seeds the sample product graph (idempotent — safe to run every start).</summary>
|
||||
public Task SeedAsync(CancellationToken ct = default) => this._runner.WriteAsync(async r =>
|
||||
{
|
||||
await r.RunAsync(
|
||||
"""
|
||||
UNWIND $products AS row
|
||||
MERGE (p:Product {name: row.name})
|
||||
SET p.category = row.category, p.brand = row.brand, p.price = row.price,
|
||||
p.in_stock = row.in_stock, p.inventory = row.inventory,
|
||||
p.description = row.description, p.popularity = row.popularity
|
||||
MERGE (c:ProductCategory {name: row.category})
|
||||
MERGE (b:ProductBrand {name: row.brand})
|
||||
MERGE (p)-[:IN_CATEGORY]->(c)
|
||||
MERGE (p)-[:MADE_BY]->(b)
|
||||
""",
|
||||
new
|
||||
{
|
||||
products = s_seed.Select(p => (object)new Dictionary<string, object>
|
||||
{
|
||||
["name"] = p.Name, ["category"] = p.Category, ["brand"] = p.Brand, ["price"] = p.Price,
|
||||
["in_stock"] = p.InStock, ["inventory"] = p.Inventory, ["description"] = p.Description,
|
||||
["popularity"] = p.Popularity,
|
||||
}).ToList(),
|
||||
});
|
||||
}, ct);
|
||||
|
||||
// ── Tools (also usable directly in the scripted demo) ────────────────────────────────────────
|
||||
|
||||
[Description("Search the product catalog for items matching a query, with optional category, brand, and max-price filters.")]
|
||||
public Task<string> SearchProductsAsync(
|
||||
[Description("What the customer is looking for, e.g. 'running shoes'.")] string query,
|
||||
[Description("Optional category filter: shoes, electronics, apparel.")] string? category = null,
|
||||
[Description("Optional brand filter, e.g. 'Nike'.")] string? brand = null,
|
||||
[Description("Optional maximum price.")] double? maxPrice = null,
|
||||
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
|
||||
{
|
||||
const string Cypher =
|
||||
"""
|
||||
MATCH (p:Product)
|
||||
WHERE ANY(w IN split(toLower($query), ' ') WHERE
|
||||
toLower(p.name) CONTAINS w OR toLower(p.description) CONTAINS w OR toLower(p.category) CONTAINS w)
|
||||
AND ($category IS NULL OR p.category = $category)
|
||||
AND ($brand IS NULL OR p.brand = $brand)
|
||||
AND ($maxPrice IS NULL OR p.price <= $maxPrice)
|
||||
RETURN p.name AS name, p.brand AS brand, p.category AS category,
|
||||
p.price AS price, p.in_stock AS inStock
|
||||
ORDER BY p.popularity DESC
|
||||
LIMIT 10
|
||||
""";
|
||||
var cursor = await r.RunAsync(Cypher, new { query, category, brand, maxPrice });
|
||||
return Render("Matches", await cursor.ToListAsync());
|
||||
}, ct);
|
||||
|
||||
[Description("Get personalized product recommendations, optionally biased toward a preferred brand and/or category.")]
|
||||
public Task<string> GetRecommendationsAsync(
|
||||
[Description("The customer's preferred brand (from their saved preferences), if known.")] string? preferredBrand = null,
|
||||
[Description("Optional category to recommend within.")] string? category = null,
|
||||
[Description("How many recommendations to return.")] int limit = 5,
|
||||
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
|
||||
{
|
||||
const string Cypher =
|
||||
"""
|
||||
MATCH (p:Product)
|
||||
WHERE p.in_stock = true
|
||||
AND ($category IS NULL OR p.category = $category)
|
||||
WITH p, (CASE WHEN $preferredBrand IS NOT NULL AND p.brand = $preferredBrand THEN 1 ELSE 0 END) AS onBrand
|
||||
RETURN p.name AS name, p.brand AS brand, p.category AS category, p.price AS price, p.in_stock AS inStock
|
||||
ORDER BY onBrand DESC, p.popularity DESC
|
||||
LIMIT $limit
|
||||
""";
|
||||
var cursor = await r.RunAsync(Cypher, new { preferredBrand, category, limit });
|
||||
var header = preferredBrand is null ? "Recommended for you" : $"Recommended for you (favoring {preferredBrand})";
|
||||
return Render(header, await cursor.ToListAsync());
|
||||
}, ct);
|
||||
|
||||
[Description("Find products related to a given product — same category or same brand — via graph traversal.")]
|
||||
public Task<string> GetRelatedProductsAsync(
|
||||
[Description("The exact product name to find related items for.")] string productName,
|
||||
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
|
||||
{
|
||||
const string Cypher =
|
||||
"""
|
||||
MATCH (p:Product {name: $productName})
|
||||
CALL (p) {
|
||||
MATCH (p)-[:IN_CATEGORY]->(c)<-[:IN_CATEGORY]-(rel:Product) WHERE rel <> p
|
||||
RETURN rel, 'same category' AS reason
|
||||
UNION
|
||||
MATCH (p)-[:MADE_BY]->(b)<-[:MADE_BY]-(rel:Product) WHERE rel <> p
|
||||
RETURN rel, 'same brand' AS reason
|
||||
}
|
||||
WITH rel, collect(DISTINCT reason) AS reasons
|
||||
RETURN rel.name AS name, rel.brand AS brand, rel.category AS category,
|
||||
rel.price AS price, rel.in_stock AS inStock, rel.popularity AS popularity,
|
||||
reduce(s = '', x IN reasons | CASE WHEN s = '' THEN x ELSE s + ', ' + x END) AS reason
|
||||
ORDER BY popularity DESC
|
||||
LIMIT 5
|
||||
""";
|
||||
var cursor = await r.RunAsync(Cypher, new { productName });
|
||||
return Render($"Related to {productName}", await cursor.ToListAsync());
|
||||
}, ct);
|
||||
|
||||
[Description("Check whether a product is in stock and how many units are available.")]
|
||||
public Task<string> CheckInventoryAsync(
|
||||
[Description("The exact product name to check.")] string productName,
|
||||
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
|
||||
{
|
||||
var cursor = await r.RunAsync(
|
||||
"MATCH (p:Product {name: $productName}) RETURN p.name AS name, p.in_stock AS inStock, p.inventory AS inventory",
|
||||
new { productName });
|
||||
var rows = await cursor.ToListAsync();
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
return $"'{productName}' was not found in the catalog.";
|
||||
}
|
||||
|
||||
var rec = rows[0];
|
||||
var inStock = rec["inStock"].As<bool>();
|
||||
return inStock
|
||||
? $"{rec["name"].As<string>()}: In stock ({rec["inventory"].As<long>()} available)."
|
||||
: $"{rec["name"].As<string>()}: Out of stock.";
|
||||
}, ct);
|
||||
|
||||
/// <summary>The retail tools as MAF/MEAI <see cref="AIFunction"/>s (attach to the agent's ChatOptions.Tools).</summary>
|
||||
public IReadOnlyList<AIFunction> CreateAIFunctions() =>
|
||||
[
|
||||
AIFunctionFactory.Create(this.SearchProductsAsync, "search_products",
|
||||
"Search the product catalog with optional category/brand/price filters."),
|
||||
AIFunctionFactory.Create(this.GetRecommendationsAsync, "get_recommendations",
|
||||
"Get personalized recommendations, optionally favoring a preferred brand/category."),
|
||||
AIFunctionFactory.Create(this.GetRelatedProductsAsync, "get_related_products",
|
||||
"Find products related to a given product via the graph."),
|
||||
AIFunctionFactory.Create(this.CheckInventoryAsync, "check_inventory",
|
||||
"Check stock/availability for a product."),
|
||||
];
|
||||
|
||||
private static string Render(string header, List<IRecord> rows)
|
||||
{
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
return $"{header}: (no matches)";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder().Append(header).Append(':').AppendLine();
|
||||
foreach (var rec in rows)
|
||||
{
|
||||
var stock = rec["inStock"].As<bool>() ? "in stock" : "out of stock";
|
||||
var reason = rec.Keys.Contains("reason") ? $" [{rec["reason"].As<string>()}]" : string.Empty;
|
||||
sb.Append(" • ")
|
||||
.Append(rec["name"].As<string>())
|
||||
.Append(" — ").Append(rec["brand"].As<string>())
|
||||
.Append(", ").Append(rec["category"].As<string>())
|
||||
.Append(", $").Append(rec["price"].As<double>().ToString("0"))
|
||||
.Append(", ").Append(stock).Append(reason)
|
||||
.AppendLine();
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET)
|
||||
//
|
||||
// A .NET port of the Neo4j Labs "agent-memory" retail-assistant example
|
||||
// (https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant,
|
||||
// referenced from https://learn.microsoft.com/en-us/agent-framework/integrations/neo4j-memory).
|
||||
//
|
||||
// A shopping assistant that LEARNS a customer's preferences and RECOMMENDS products via graph
|
||||
// traversal, backed by DURABLE memory in Neo4j. It uses the AgentMemory library — a .NET port of the
|
||||
// Python memory provider, not an officially recognized Neo4j integration — and its Microsoft Agent
|
||||
// Framework adapter:
|
||||
// • Neo4jMemoryContextProvider (an AIContextProvider) — recalls memory before each run, persists
|
||||
// after, and (via ExposeMemoryToolsFromContextProvider) surfaces the memory tools (search/remember/
|
||||
// recall) itself through AIContext.Tools
|
||||
// • ProductCatalog.CreateAIFunctions() — retail tools over a Neo4j :Product graph
|
||||
//
|
||||
// Configuration (environment variables, matching the other Foundry samples):
|
||||
// AZURE_OPENAI_ENDPOINT (required) — your Azure OpenAI / Foundry endpoint
|
||||
// AZURE_OPENAI_API_KEY (optional) — API key; if unset, DefaultAzureCredential (az login) is used
|
||||
// FOUNDRY_MODEL (default: gpt-4o-mini) — chat model deployment
|
||||
// FOUNDRY_EMBEDDING_MODEL (default: text-embedding-3-small) — embedding model deployment (1536 dims)
|
||||
// NEO4J_URI (default: bolt://localhost:7687)
|
||||
// NEO4J_USER (default: neo4j)
|
||||
// NEO4J_PASSWORD (default: password)
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using AgentMemory.Abstractions.Services;
|
||||
using AgentMemory.AgentFramework;
|
||||
using AgentMemory.Core;
|
||||
using AgentMemory.Core.Stubs;
|
||||
using AgentMemory.Neo4j.Infrastructure;
|
||||
using AgentMemoryShoppingAssistant;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenAI;
|
||||
|
||||
// ── Model + credentials (Azure OpenAI / Foundry, via env vars) ───────────────────────────────────
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
var chatModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o-mini";
|
||||
var embeddingModel = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-small";
|
||||
|
||||
var clientOptions = new OpenAIClientOptions { Endpoint = new Uri(endpoint) };
|
||||
// API key if provided, otherwise Azure credential (dev: `az login`).
|
||||
// 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.
|
||||
OpenAIClient openAI = string.IsNullOrWhiteSpace(apiKey)
|
||||
? new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), clientOptions)
|
||||
: new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
|
||||
|
||||
IChatClient chatClient = openAI.GetChatClient(chatModel).AsIChatClient();
|
||||
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator =
|
||||
openAI.GetEmbeddingClient(embeddingModel).AsIEmbeddingGenerator();
|
||||
|
||||
// ── AgentMemory (Neo4j) DI ───────────────────────────────────────────────────────────────────────
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Warning);
|
||||
|
||||
builder.Services.AddNeo4jAgentMemory(options =>
|
||||
{
|
||||
options.Uri = Environment.GetEnvironmentVariable("NEO4J_URI") ?? "bolt://localhost:7687";
|
||||
options.Username = Environment.GetEnvironmentVariable("NEO4J_USER") ?? "neo4j";
|
||||
options.Password = Environment.GetEnvironmentVariable("NEO4J_PASSWORD") ?? "password";
|
||||
});
|
||||
builder.Services.AddAgentMemoryCore(_ => { });
|
||||
builder.Services.AddSingleton<IClock, SystemClock>();
|
||||
builder.Services.AddSingleton<IIdGenerator, GuidIdGenerator>();
|
||||
builder.Services.TryAddSingleton(chatClient);
|
||||
builder.Services.TryAddSingleton(embeddingGenerator);
|
||||
builder.Services.AddAgentMemoryFramework(options =>
|
||||
{
|
||||
options.AutoExtractOnPersist = true;
|
||||
options.ContextFormat.IncludeEntities = true;
|
||||
options.ContextFormat.IncludeFacts = true;
|
||||
options.ContextFormat.IncludePreferences = true;
|
||||
options.ExposeMemoryToolsFromContextProvider = true;
|
||||
});
|
||||
|
||||
var host = builder.Build();
|
||||
await using var hostDisposal = (IAsyncDisposable)host;
|
||||
|
||||
await using var scope = host.Services.CreateAsyncScope();
|
||||
var sp = scope.ServiceProvider;
|
||||
|
||||
// ── Setup: schema + sample product graph ─────────────────────────────────────────────────────────
|
||||
var catalog = new ProductCatalog(sp.GetRequiredService<INeo4jTransactionRunner>());
|
||||
await sp.GetRequiredService<ISchemaBootstrapper>().BootstrapAsync();
|
||||
await catalog.SeedAsync();
|
||||
Console.WriteLine("Neo4j schema ready; sample products loaded.\n");
|
||||
|
||||
// ── The shopping assistant: context provider (recall + memory tools) + product tools ─────────────
|
||||
var memoryProvider = sp.GetRequiredService<Neo4jMemoryContextProvider>();
|
||||
var productTools = catalog.CreateAIFunctions();
|
||||
|
||||
// WithMemoryOwnerScoping(sp) scopes the whole invocation (recall, tool calls, persistence) to the
|
||||
// owner set via WithMemoryIdentity below — no manual BeginOwnerScope wrapping needed per turn.
|
||||
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "ShoppingAssistant",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = chatModel,
|
||||
Instructions =
|
||||
"You are a helpful shopping assistant for an online store. Learn and remember the customer's "
|
||||
+ "preferences (brands, budget, categories) using the memory tools, and recommend products that "
|
||||
+ "fit using the product tools. Explain why each recommendation matches, and suggest alternatives "
|
||||
+ "when something is out of stock.",
|
||||
// memoryProvider appends the six memory tools (search_memory, remember_fact, ...) to this list
|
||||
// on every model call via AIContext.Tools — see ExposeMemoryToolsFromContextProvider above.
|
||||
Tools = [.. productTools],
|
||||
},
|
||||
AIContextProviders = [memoryProvider],
|
||||
}).WithMemoryOwnerScoping(sp);
|
||||
|
||||
const string Shopper = "shopper-amelia";
|
||||
|
||||
// ── Session A — the customer shops; the model calls the tools and remembers preferences ──────────
|
||||
Console.WriteLine(">> Session A\n");
|
||||
var sessionA = (await agent.CreateSessionAsync())
|
||||
.WithMemoryIdentity(userId: Shopper, sessionId: "cart-a", applicationId: "retail-demo");
|
||||
|
||||
foreach (var turn in new[]
|
||||
{
|
||||
"Hi! I'm looking for running shoes. I love Nike and want to stay under $150.",
|
||||
"Nice — what would you recommend for me, and is anything I might like out of stock?",
|
||||
})
|
||||
{
|
||||
await SayAsync(agent, sessionA, turn);
|
||||
}
|
||||
|
||||
// ── Session B — a NEW session for the same shopper still recalls her preferences ─────────────────
|
||||
Console.WriteLine(">> Session B — a brand-new session; memory is durable\n");
|
||||
var sessionB = (await agent.CreateSessionAsync())
|
||||
.WithMemoryIdentity(userId: Shopper, sessionId: "cart-b", applicationId: "retail-demo");
|
||||
|
||||
await SayAsync(agent, sessionB, "I'm back — remind me what I like and suggest something new.");
|
||||
|
||||
Console.WriteLine("=== Done. Preferences + messages persist in Neo4j across sessions. ===");
|
||||
|
||||
// One conversational turn. Owner scoping (recall, tool calls, and persistence) is guaranteed
|
||||
// automatically by the WithMemoryOwnerScoping-wrapped agent — no manual BeginOwnerScope needed here.
|
||||
static async Task SayAsync(AIAgent agent, AgentSession session, string message)
|
||||
{
|
||||
Console.WriteLine($"USER : {message}");
|
||||
var response = await agent.RunAsync(message, session);
|
||||
Console.WriteLine($"ASSISTANT : {response.Text}\n");
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# Agent with Memory Using AgentMemory — Shopping Assistant
|
||||
|
||||
A **.NET port of the Neo4j Labs "agent-memory" retail assistant** example
|
||||
([`microsoft_agent_retail_assistant`](https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant),
|
||||
referenced from the [Learn integration page](https://learn.microsoft.com/en-us/agent-framework/integrations/neo4j-memory)).
|
||||
A shopping assistant that **learns a customer's preferences** and **recommends products via graph
|
||||
traversal**, backed by durable memory in Neo4j.
|
||||
|
||||
It uses the [`AgentMemory`](https://www.nuget.org/packages/AgentMemory) library — a .NET port of the
|
||||
(Python-only) Neo4j Labs memory provider, **not an officially recognized Neo4j integration** — through
|
||||
its Microsoft Agent Framework adapter.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- **`Neo4jMemoryContextProvider`** (an `AIContextProvider`) — recalls relevant memory before each run,
|
||||
persists new memory after (the same bidirectional pattern as the official provider), and — via
|
||||
`ExposeMemoryToolsFromContextProvider = true` — surfaces the memory tools (search / remember / recall)
|
||||
itself through `AIContext.Tools`.
|
||||
- **`ProductCatalog.CreateAIFunctions()`** — retail tools over a Neo4j `:Product` graph (search /
|
||||
recommend / related / inventory).
|
||||
- Preference learning that persists across a brand-new `AgentSession` for the same shopper.
|
||||
- Graph-based product recommendations and "related products" via traversal.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- A **Neo4j 5.x** instance (the sample bootstraps the schema and seeds sample products)
|
||||
- An **Azure OpenAI / Foundry** deployment (a chat model + an embedding model)
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
| Variable | Required | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `AZURE_OPENAI_ENDPOINT` | ✅ | — | Azure OpenAI / Foundry endpoint |
|
||||
| `AZURE_OPENAI_API_KEY` | — | — | API key; if unset, `DefaultAzureCredential` (`az login`) is used |
|
||||
| `FOUNDRY_MODEL` | — | `gpt-4o-mini` | chat model deployment |
|
||||
| `FOUNDRY_EMBEDDING_MODEL` | — | `text-embedding-3-small` | embedding model deployment (1536 dims) |
|
||||
| `NEO4J_URI` | — | `bolt://localhost:7687` | Neo4j bolt URI |
|
||||
| `NEO4J_USER` | — | `neo4j` | Neo4j user |
|
||||
| `NEO4J_PASSWORD` | — | `password` | Neo4j password |
|
||||
|
||||
> Ensure the embedding model's dimensions match the Neo4j vector-index dimensions AgentMemory bootstraps
|
||||
> (default 1536, which matches `text-embedding-3-small`).
|
||||
|
||||
## Run the Sample
|
||||
|
||||
```bash
|
||||
docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j:5.26
|
||||
|
||||
export AZURE_OPENAI_ENDPOINT="https://<your-resource>.openai.azure.com"
|
||||
export AZURE_OPENAI_API_KEY="<your-key>" # or omit and `az login`
|
||||
export FOUNDRY_MODEL="gpt-4o-mini"
|
||||
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
1. The sample bootstraps the Neo4j schema and seeds a small product graph (`:Product`,
|
||||
`:ProductCategory`, `:ProductBrand` nodes).
|
||||
2. **Session A** — the shopper says she wants running shoes, loves Nike, and has a $150 budget; the
|
||||
agent calls the memory tools to remember this and the product tools to recommend matching items.
|
||||
3. **Session B** — a brand-new session for the same shopper (`shopper-amelia`) still recalls her
|
||||
preferences and can suggest something new, because memory persists in Neo4j across sessions.
|
||||
|
||||
## Note on packaging
|
||||
|
||||
This sample is part of the repo's solution and targets .NET 10 like every other sample, but it
|
||||
deliberately opts out of **Central Package Management** and does **not** reference `Microsoft.Agents.AI`
|
||||
via the repo's in-source project — it consumes the **published** `AgentMemory` NuGet packages instead
|
||||
(which target `Microsoft.Agents.AI` 1.9.0). A version that references the repo's current
|
||||
`Microsoft.Agents.AI` source would require AgentMemory to be rebuilt against that version first.
|
||||
@@ -9,6 +9,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|
||||
|[Custom Memory Implementation](../../01-get-started/04_memory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
|
||||
|[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.|
|
||||
|[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.|
|
||||
|[Memory Using AgentMemory](./AgentWithMemory_Step06_MemoryUsingAgentMemory/)|This sample demonstrates a retail shopping assistant built with [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), an unofficial .NET port of the Neo4j Labs graph-memory provider, to learn customer preferences and recommend products via graph traversal.|
|
||||
|
||||
> **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ AIProjectClient aiProjectClient = new(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// Create an In-Memory vector store that uses the Azure AI Foundry embedding model to generate embeddings.
|
||||
// Create an In-Memory vector store that uses the Microsoft Foundry embedding model to generate embeddings.
|
||||
VectorStore vectorStore = new InMemoryVectorStore(new()
|
||||
{
|
||||
EmbeddingGenerator = aiProjectClient.GetProjectOpenAIClient().GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ AIProjectClient aiProjectClient = new(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// Create a Qdrant vector store that uses the Azure AI Foundry embedding model to generate embeddings.
|
||||
// Create a Qdrant vector store that uses the Microsoft Foundry embedding model to generate embeddings.
|
||||
QdrantClient client = new("localhost");
|
||||
VectorStore vectorStore = new QdrantVectorStore(client, ownsClient: true, new()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Structured Output — Configure agents to return typed JSON
|
||||
//
|
||||
// This sample shows how to configure a ChatClientAgent to produce
|
||||
// structured output using JSON schema constraints with Azure AI Foundry.
|
||||
// structured output using JSON schema constraints with Microsoft Foundry.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Agent Observability — OpenTelemetry tracing with Azure AI Foundry
|
||||
// Agent Observability — OpenTelemetry tracing with Microsoft Foundry
|
||||
//
|
||||
// This sample shows how to instrument an AI agent with OpenTelemetry
|
||||
// for distributed tracing and telemetry logging.
|
||||
|
||||
@@ -19,7 +19,7 @@ var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt
|
||||
// Create a host builder that we will register services with and then run.
|
||||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Create the AI agent from the Azure AI Foundry project client.
|
||||
// Create the AI agent from the Microsoft Foundry project client.
|
||||
// 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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Middleware — Chain multiple middleware layers on an agent
|
||||
//
|
||||
// This sample shows multiple middleware layers working together with Azure AI Foundry:
|
||||
// This sample shows multiple middleware layers working together with Microsoft Foundry:
|
||||
// chat client (global/per-request), agent run (PII filtering and guardrails),
|
||||
// function invocation (logging and result overrides), human-in-the-loop
|
||||
// approval workflows for sensitive function calls, and MessageAIContextProvider
|
||||
@@ -15,7 +15,7 @@ using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
// Get Azure AI Foundry configuration from environment variables
|
||||
// Get Microsoft Foundry configuration from environment variables
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Background Responses — Asynchronous agent execution with polling
|
||||
//
|
||||
// This sample shows how to use background responses with ChatClientAgent
|
||||
// and Azure AI Foundry for non-blocking agent execution.
|
||||
// and Microsoft Foundry for non-blocking agent execution.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
|
||||
@@ -67,7 +67,7 @@ static string GetTime([Description("The city name.")] string city) =>
|
||||
// asking for alternative destinations. The model will process this injected message on the next
|
||||
// service call — even though the parent FunctionInvokingChatClient loop would otherwise stop.
|
||||
[Description("Check current travel advisories for a city.")]
|
||||
static string CheckTravelAdvisory([Description("The city name.")] string city)
|
||||
static async Task<string> CheckTravelAdvisory([Description("The city name.")] string city)
|
||||
{
|
||||
// Simulated travel advisory data.
|
||||
var advisory = city.ToUpperInvariant() switch
|
||||
@@ -85,9 +85,13 @@ static string CheckTravelAdvisory([Description("The city name.")] string city)
|
||||
// When an advisory is found, inject a follow-up question so the model automatically
|
||||
// suggests alternatives without the user needing to ask.
|
||||
var runContext = AIAgent.CurrentRunContext!;
|
||||
runContext.Agent.GetService<MessageInjectingChatClient>()?.EnqueueMessages(
|
||||
runContext.Session!,
|
||||
[new ChatMessage(ChatRole.User, $"Given the travel advisory for {city}, what alternative cities would you recommend instead?")]);
|
||||
var injector = runContext.Agent.GetService<MessageInjectingChatClient>();
|
||||
if (injector is not null)
|
||||
{
|
||||
await injector.EnqueueMessagesAsync(
|
||||
runContext.Session!,
|
||||
[new ChatMessage(ChatRole.User, $"Given the travel advisory for {city}, what alternative cities would you recommend instead?")]);
|
||||
}
|
||||
|
||||
return advisory;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- User has the required role to invoke models in the Foundry project.
|
||||
|
||||
**Note**: These samples use models hosted through Microsoft Foundry. For more information, see [Azure AI Foundry documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/).
|
||||
**Note**: These samples use models hosted through Microsoft Foundry. For more information, see [Microsoft Foundry documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/).
|
||||
|
||||
**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Foundry project. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ The simplest agent evaluation: create a Foundry agent, run it against test quest
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure authentication available to `DefaultAzureCredential` (for local development, run `az login`)
|
||||
- A deployed model in your Azure AI Foundry project
|
||||
- A deployed model in your Microsoft Foundry project
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
|
||||
+6
@@ -14,6 +14,12 @@ It builds on Post 1's personal finance assistant and teaches it to work with *yo
|
||||
saving and deleting still pause for approval. The `place_trade` tool is also wrapped in an
|
||||
`ApprovalRequiredAIFunction` (see `TradingTools.cs`), so the harness surfaces an approval prompt
|
||||
before any trade runs. The trade itself is simulated — no real order is placed.
|
||||
|
||||
> ⚠️ **Security — avoid tool-name collisions:** auto-approval rules such as
|
||||
> `FileAccessProvider.ReadOnlyToolsAutoApprovalRule` match tool calls **solely by tool name**. Any
|
||||
> other registered tool that shares one of the approved names (`file_access_read`, `file_access_ls`,
|
||||
> `file_access_grep`) would be silently auto-approved, bypassing the human
|
||||
> approval boundary. Ensure no other tool's name collides with the reserved names a rule approves.
|
||||
- **Durable memory, two ways:**
|
||||
- **File memory** (coarse-grained, explicit) — the agent reads/writes files such as
|
||||
`watchlist.md`. File memory is on by default; its files live on disk under
|
||||
|
||||
+7
-5
@@ -133,7 +133,7 @@ AIAgent researchAgent = ResearchAgent.Create(chatClient);
|
||||
// A sandboxed shell, confined to the trade-confirmation vault. ConfineWorkingDirectory re-anchors
|
||||
// every command to the vault, and the deny-list policy pre-filters obviously destructive commands.
|
||||
// (Patterns are a UX guardrail, not a security boundary — for hard isolation use DockerShellExecutor.)
|
||||
await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
|
||||
await using var shellExecutor = new LocalShellExecutor(new LocalShellExecutorOptions
|
||||
{
|
||||
WorkingDirectory = vaultDir,
|
||||
ConfineWorkingDirectory = true,
|
||||
@@ -160,7 +160,9 @@ using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptio
|
||||
// Turn the chat client into a HarnessAgent. On top of Post 2's file access and approvals we add the
|
||||
// four "scaling" capabilities: skills (our own provider), background agents, a confined shell, and
|
||||
// CodeAct.
|
||||
List<AIContextProvider> contextProviders = [skillsProvider, codeAct];
|
||||
// The shell is wired up in two parts: the ShellEnvironmentProvider injects OS/shell/CWD info into the
|
||||
// system prompt, and the shell tool is registered below in ChatOptions.
|
||||
List<AIContextProvider> contextProviders = [skillsProvider, codeAct, new ShellEnvironmentProvider(shellExecutor)];
|
||||
|
||||
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
|
||||
{
|
||||
@@ -170,8 +172,6 @@ AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
|
||||
DisableAgentSkillsProvider = true,
|
||||
// Fan-out research is delegated to this background agent.
|
||||
BackgroundAgents = [researchAgent],
|
||||
// The confined shell, exposed as the approval-gated run_shell tool.
|
||||
ShellExecutor = shell,
|
||||
// Keep reading the portfolio frictionless while writes, trades, and shell commands still prompt.
|
||||
ToolApprovalAgentOptions = new ToolApprovalAgentOptions
|
||||
{
|
||||
@@ -179,7 +179,7 @@ AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
|
||||
},
|
||||
// Start in "execute" mode for quick lookups and actions; switch any time with /mode plan.
|
||||
AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
|
||||
// Our skills provider plus CodeAct.
|
||||
// Our skills provider, CodeAct, and the shell environment provider.
|
||||
AIContextProviders = contextProviders,
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
@@ -188,6 +188,8 @@ AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
|
||||
[
|
||||
StockTools.CreateGetStockPriceTool(),
|
||||
TradingTools.CreatePlaceTradeTool(),
|
||||
// The confined shell, exposed as the approval-gated run_shell tool.
|
||||
shellExecutor.AsAIFunction(requireApproval: true),
|
||||
],
|
||||
Reasoning = new() { Effort = ReasoningEffort.Medium },
|
||||
},
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ public sealed class ModeCommandHandler : CommandHandler
|
||||
string[] parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
string current = this._modeProvider.GetMode(session);
|
||||
string current = await this._modeProvider.GetModeAsync(session).ConfigureAwait(false);
|
||||
await ux.WriteInfoLineAsync($"Current mode: {current}").ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public sealed class ModeCommandHandler : CommandHandler
|
||||
|
||||
try
|
||||
{
|
||||
this._modeProvider.SetMode(session, newMode);
|
||||
await this._modeProvider.SetModeAsync(session, newMode).ConfigureAwait(false);
|
||||
ux.CurrentMode = newMode;
|
||||
await ux.WriteInfoLineAsync($"Switched to {newMode} mode.", ModeColors.Get(newMode, this._modeColors)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
{
|
||||
if (await handler.TryHandleAsync(text, this._session, this._ux).ConfigureAwait(false))
|
||||
{
|
||||
this._ux.CurrentMode = this._modeProvider?.GetMode(this._session);
|
||||
this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -111,16 +111,15 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
/// enqueued via the <see cref="MessageInjectingChatClient"/> so it can be picked up
|
||||
/// by the agent on its next opportunity.
|
||||
/// </summary>
|
||||
internal Task OnStreamingInputAsync(string text)
|
||||
internal async Task OnStreamingInputAsync(string text)
|
||||
{
|
||||
if (this._messageInjector is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
this._messageInjector.EnqueueMessages(this._session, [new ChatMessage(ChatRole.User, text)]);
|
||||
this._ux.SetQueuedMessages(this._messageInjector.GetPendingMessages(this._session));
|
||||
return Task.CompletedTask;
|
||||
await this._messageInjector.EnqueueMessagesAsync(this._session, [new ChatMessage(ChatRole.User, text)]).ConfigureAwait(false);
|
||||
this._ux.SetQueuedMessages(await this._messageInjector.GetPendingMessagesAsync(this._session).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,7 +135,7 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
{
|
||||
if (messages.Count == 0)
|
||||
{
|
||||
this.CompleteTurn();
|
||||
await this.CompleteTurnAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,17 +150,19 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
private async Task RunAgentLoopAsync(IList<ChatMessage> messages)
|
||||
{
|
||||
IList<ChatMessage>? nextMessages = messages;
|
||||
IReadOnlyList<ChatMessage> lastPendingMessages = this._messageInjector?.GetPendingMessages(this._session) ?? [];
|
||||
IReadOnlyList<ChatMessage> lastPendingMessages = this._messageInjector is not null
|
||||
? await this._messageInjector.GetPendingMessagesAsync(this._session).ConfigureAwait(false)
|
||||
: [];
|
||||
|
||||
while (nextMessages is not null)
|
||||
{
|
||||
var runOptions = new AgentRunOptions();
|
||||
foreach (var observer in this._observers)
|
||||
{
|
||||
observer.ConfigureRunOptions(runOptions, this._agent, this._session);
|
||||
await observer.ConfigureRunOptionsAsync(runOptions, this._agent, this._session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._ux.CurrentMode = this._modeProvider?.GetMode(this._session);
|
||||
this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
|
||||
this._ux.BeginStreaming();
|
||||
this._ux.BeginStreamingOutput();
|
||||
|
||||
@@ -171,7 +172,7 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
{
|
||||
if (this._modeProvider is not null)
|
||||
{
|
||||
string currentMode = this._modeProvider.GetMode(this._session);
|
||||
string currentMode = await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
|
||||
if (currentMode != this._ux.CurrentMode)
|
||||
{
|
||||
this._ux.CurrentMode = currentMode;
|
||||
@@ -199,7 +200,7 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
this.SyncQueuedMessageDisplay(ref lastPendingMessages);
|
||||
lastPendingMessages = await this.SyncQueuedMessageDisplayAsync(lastPendingMessages).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -208,7 +209,7 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
}
|
||||
|
||||
// Final sync after streaming.
|
||||
this.SyncQueuedMessageDisplay(ref lastPendingMessages);
|
||||
lastPendingMessages = await this.SyncQueuedMessageDisplayAsync(lastPendingMessages).ConfigureAwait(false);
|
||||
|
||||
this._ux.StopSpinner();
|
||||
await this._ux.EndStreamingOutputAsync().ConfigureAwait(false);
|
||||
@@ -261,13 +262,13 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
nextMessages = drained.Count > 0 ? [.. drained] : null;
|
||||
}
|
||||
|
||||
this.CompleteTurn();
|
||||
await this.CompleteTurnAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void CompleteTurn()
|
||||
private async Task CompleteTurnAsync()
|
||||
{
|
||||
this._ux.EndStreaming();
|
||||
this._ux.CurrentMode = this._modeProvider?.GetMode(this._session);
|
||||
this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -275,14 +276,15 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
/// Messages that have been consumed (drained by the service) are echoed to the output
|
||||
/// area as regular user-input entries.
|
||||
/// </summary>
|
||||
private void SyncQueuedMessageDisplay(ref IReadOnlyList<ChatMessage> lastPendingMessages)
|
||||
/// <returns>The updated snapshot of pending messages.</returns>
|
||||
private async Task<IReadOnlyList<ChatMessage>> SyncQueuedMessageDisplayAsync(IReadOnlyList<ChatMessage> lastPendingMessages)
|
||||
{
|
||||
if (this._messageInjector is null)
|
||||
{
|
||||
return;
|
||||
return lastPendingMessages;
|
||||
}
|
||||
|
||||
var pending = this._messageInjector.GetPendingMessages(this._session);
|
||||
var pending = await this._messageInjector.GetPendingMessagesAsync(this._session).ConfigureAwait(false);
|
||||
|
||||
int consumedCount = lastPendingMessages.Count - pending.Count;
|
||||
for (int i = 0; i < consumedCount && i < lastPendingMessages.Count; i++)
|
||||
@@ -291,7 +293,7 @@ public sealed class HarnessAgentRunner : IDisposable
|
||||
this._ux.WriteUserInputEcho(text);
|
||||
}
|
||||
|
||||
lastPendingMessages = pending;
|
||||
this._ux.SetQueuedMessages(pending);
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,11 @@ public static class HarnessConsole
|
||||
? await options.SessionFactory(agent)
|
||||
: await agent.CreateSessionAsync();
|
||||
|
||||
string? initialMode = modeProvider is null ? null : await modeProvider.GetModeAsync(session);
|
||||
|
||||
using var component = new HarnessAppComponent(
|
||||
placeholder: userPrompt,
|
||||
initialMode: modeProvider?.GetMode(session),
|
||||
initialMode: initialMode,
|
||||
inputEnabled: messageInjector is not null,
|
||||
runnerFactory: ux => new HarnessAgentRunner(
|
||||
agent: agent,
|
||||
|
||||
+1
-3
@@ -20,9 +20,7 @@ public abstract class ConsoleObserver
|
||||
/// <param name="options">The run options to configure.</param>
|
||||
/// <param name="agent">The agent being interacted with.</param>
|
||||
/// <param name="session">The current agent session.</param>
|
||||
public virtual void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session)
|
||||
{
|
||||
}
|
||||
public virtual ValueTask ConfigureRunOptionsAsync(AgentRunOptions options, AIAgent agent, AgentSession session) => default;
|
||||
|
||||
/// <summary>
|
||||
/// Called for each <see cref="AgentResponseUpdate"/> in the response stream, regardless of
|
||||
|
||||
+3
-3
@@ -40,9 +40,9 @@ public sealed class PlanningOutputObserver : ConsoleObserver
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session)
|
||||
public override async ValueTask ConfigureRunOptionsAsync(AgentRunOptions options, AIAgent agent, AgentSession session)
|
||||
{
|
||||
if (this.IsPlanningMode(this._modeProvider.GetMode(session)))
|
||||
if (this.IsPlanningMode(await this._modeProvider.GetModeAsync(session).ConfigureAwait(false)))
|
||||
{
|
||||
options.ResponseFormat = ChatResponseFormat.ForJsonSchema<PlanningResponse>();
|
||||
}
|
||||
@@ -205,7 +205,7 @@ public sealed class PlanningOutputObserver : ConsoleObserver
|
||||
|
||||
if (selection == ApproveOption)
|
||||
{
|
||||
this._modeProvider.SetMode(session, this._executionModeName);
|
||||
await this._modeProvider.SetModeAsync(session, this._executionModeName).ConfigureAwait(false);
|
||||
await ux.WriteInfoLineAsync(
|
||||
$"✅ Switched to {this._executionModeName} mode.",
|
||||
ModeColors.Get(this._executionModeName, this._modeColors)).ConfigureAwait(false);
|
||||
|
||||
@@ -85,7 +85,6 @@ AIAgent agent =
|
||||
MaxOutputTokens = MaxOutputTokens,
|
||||
Name = "ResearchAgent",
|
||||
Description = "A research assistant that plans and executes research tasks.",
|
||||
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
|
||||
OpenTelemetrySourceName = TracingSourceName, // Use our custom source name so spans are captured by the TracerProvider above.
|
||||
FileMemoryStore = new FileSystemAgentFileStore( // Configure the file memory provider to store files in a local folder called "agent-files".
|
||||
Path.Combine(AppContext.BaseDirectory, "agent-files")),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
This sample demonstrates how to use a `HarnessAgent` with the Harness `AIContextProviders` (`TodoProvider` and `AgentModeProvider`) for interactive research tasks with web search capabilities powered by Azure AI Foundry. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and context-window compaction.
|
||||
This sample demonstrates how to use a `HarnessAgent` with the Harness `AIContextProviders` (`TodoProvider` and `AgentModeProvider`) for interactive research tasks with web search capabilities powered by Microsoft Foundry. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and context-window compaction.
|
||||
|
||||
Key features showcased:
|
||||
|
||||
@@ -19,7 +19,7 @@ Key features showcased:
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
|
||||
1. A Microsoft Foundry project with a deployed model (e.g., `gpt-5.4`)
|
||||
2. Azure CLI installed and authenticated (`az login`)
|
||||
|
||||
## Environment Variables
|
||||
@@ -27,7 +27,7 @@ Before running this sample, ensure you have:
|
||||
Set the following environment variables:
|
||||
|
||||
```bash
|
||||
# Required: Your Azure AI Foundry OpenAI endpoint
|
||||
# Required: Your Microsoft Foundry OpenAI endpoint
|
||||
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
|
||||
|
||||
# Optional: Model deployment name (defaults to gpt-5.4)
|
||||
|
||||
-2
@@ -57,7 +57,6 @@ AIAgent webSearchAgent =
|
||||
DisableTodoProvider = true,
|
||||
DisableAgentModeProvider = true,
|
||||
DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session
|
||||
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
|
||||
DisableToolAutoApproval = true, // If true, this disables the don't-ask-again approval functionality.
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
@@ -107,7 +106,6 @@ AIAgent parentAgent =
|
||||
DisableTodoProvider = true,
|
||||
DisableAgentModeProvider = true,
|
||||
DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session
|
||||
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
|
||||
DisableToolAutoApproval = true, // If true, this disables the don't-ask-again approval functionality.
|
||||
DisableWebSearch = true,
|
||||
BackgroundAgents = [webSearchAgent],
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ A parent agent receives a list of stock tickers and uses a web-search background
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An Azure AI Foundry endpoint with an OpenAI model deployment
|
||||
- A Microsoft Foundry endpoint with an OpenAI model deployment
|
||||
- Set the following environment variables:
|
||||
- `AZURE_FOUNDRY_OPENAI_ENDPOINT` — Your Foundry OpenAI endpoint URL
|
||||
- `FOUNDRY_MODEL` — Model deployment name (defaults to `gpt-5.4`)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use a HarnessAgent with the default FileAccessProvider
|
||||
// This sample demonstrates how to use a HarnessAgent with the FileAccessProvider
|
||||
// to give an agent access to a folder of CSV data files. The agent can read, analyze,
|
||||
// and extract information from the data, then write results back as new files.
|
||||
//
|
||||
// The sample includes a pre-populated `working/` folder with sales transaction data.
|
||||
// The HarnessAgent's default FileAccessProvider uses `{cwd}/working` as its working directory,
|
||||
// which matches this sample's folder layout.
|
||||
// File access is opt-in: setting HarnessAgentOptions.FileAccessStore enables the
|
||||
// FileAccessProvider, and this sample points it at the `working/` folder below the location of the executable.
|
||||
// Ask the agent to analyze the data, produce summaries, or create new output files.
|
||||
//
|
||||
// Special commands:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
This sample demonstrates how to use a `HarnessAgent` with the default `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, and opt out of unused features.
|
||||
This sample demonstrates how to use a `HarnessAgent` with the `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, a `FileAccessStore`, and opt out of unused features.
|
||||
|
||||
Key features showcased:
|
||||
|
||||
- **HarnessAgent** — a pre-configured agent that wraps a `ChatClientAgent` with function invocation, per-service-call persistence, and context-window compaction
|
||||
- **FileAccessProvider** — the HarnessAgent's default file access provider uses `{cwd}/working` as its working directory, matching this sample's `working/` folder
|
||||
- **FileAccessProvider** — file access is opt-in; setting `HarnessAgentOptions.FileAccessStore` to the sample's `working/` folder enables the provider's read/write tools
|
||||
- **CSV data processing** — the agent reads sales transaction data and performs analysis on demand
|
||||
- **Output file creation** — the agent can write summaries, filtered data, or reports back to the data folder
|
||||
- **Streaming output** — responses are streamed token-by-token for a natural experience
|
||||
@@ -15,7 +15,7 @@ Key features showcased:
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
|
||||
1. A Microsoft Foundry project with a deployed model (e.g., `gpt-5.4`)
|
||||
2. Azure CLI installed and authenticated (`az login`)
|
||||
|
||||
## Environment Variables
|
||||
@@ -23,7 +23,7 @@ Before running this sample, ensure you have:
|
||||
Set the following environment variables:
|
||||
|
||||
```bash
|
||||
# Required: Your Azure AI Foundry OpenAI endpoint
|
||||
# Required: Your Microsoft Foundry OpenAI endpoint
|
||||
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
|
||||
|
||||
# Optional: Model deployment name (defaults to gpt-5.4)
|
||||
@@ -51,6 +51,15 @@ You can ask the agent to:
|
||||
|
||||
E.g. try the following prompt `Please process the sales.csv file by first filtering it to only North region sales, and then calculating the sum of sales by person. I'd like to write the results of the processing to north_region_totals.csv`.
|
||||
|
||||
## ⚠️ Security: avoid tool-name collisions
|
||||
|
||||
This sample uses `FileAccessProvider.ReadOnlyToolsAutoApprovalRule` to auto-approve read-only file
|
||||
access tools. Built-in auto-approval rules match tool calls **solely by tool name**, so any other
|
||||
registered tool that shares one of the approved names (`file_access_read`, `file_access_ls`,
|
||||
`file_access_grep`) would be **silently auto-approved**, bypassing the
|
||||
human approval boundary. Ensure no other tool's name collides with the reserved names an
|
||||
auto-approval rule approves.
|
||||
|
||||
## Sample Data
|
||||
|
||||
The included `working/sales.csv` contains sales transactions from January to March 2025 with the following columns:
|
||||
|
||||
@@ -82,8 +82,9 @@ var instructions =
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
// Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct.
|
||||
// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess,
|
||||
// ToolApproval, WebSearch, and AgentSkillsProvider are all active.
|
||||
// TodoProvider, AgentModeProvider, FileMemory, ToolApproval, WebSearch, and
|
||||
// AgentSkillsProvider are on by default. File access is opt-in, so it is enabled here by
|
||||
// supplying a FileAccessStore.
|
||||
AIAgent agent =
|
||||
new AIProjectClient(
|
||||
new Uri(endpoint),
|
||||
@@ -101,6 +102,8 @@ AIAgent agent =
|
||||
OpenTelemetrySourceName = TracingSourceName,
|
||||
// Point the file memory at a local folder for persistent memory across sessions.
|
||||
FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")),
|
||||
// Enable file access (opt-in) by rooting the file access tools at a local working folder.
|
||||
FileAccessStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "working")),
|
||||
// Add the HyperlightCodeActProvider so the agent can execute Python code in a sandbox.
|
||||
AIContextProviders = [codeAct],
|
||||
ChatOptions = new ChatOptions
|
||||
|
||||
@@ -10,14 +10,14 @@ The agent can plan tasks, manage modes, store memories, read/write files, search
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- An Azure AI Foundry project endpoint
|
||||
- A Microsoft Foundry project endpoint
|
||||
- KVM-capable host (the Hyperlight sandbox runs code in micro-VMs)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint |
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Your Microsoft Foundry project endpoint |
|
||||
| `FOUNDRY_MODEL` | Model deployment name (default: `gpt-5.4`) |
|
||||
|
||||
## Running
|
||||
|
||||
@@ -174,9 +174,9 @@ async Task ApprovalLoopAsync()
|
||||
{
|
||||
AutoApprovalRules =
|
||||
[
|
||||
functionCall =>
|
||||
context =>
|
||||
{
|
||||
Console.WriteLine($" Auto-approving: {functionCall.Name}");
|
||||
Console.WriteLine($" Auto-approving: {context.FunctionCallContent.Name}");
|
||||
return ValueTask.FromResult(true);
|
||||
},
|
||||
],
|
||||
@@ -252,7 +252,6 @@ AIAgent CreateLeanHarnessAgent(
|
||||
DisableAgentModeProvider = true,
|
||||
DisableTodoProvider = disableTodoProvider,
|
||||
DisableFileMemory = true,
|
||||
DisableFileAccess = true,
|
||||
DisableWebSearch = true,
|
||||
ToolApprovalAgentOptions = toolApprovalAgentOptions,
|
||||
ChatOptions = new ChatOptions
|
||||
|
||||
@@ -32,7 +32,7 @@ The Python sample in [microsoft/agent-framework#6174](https://github.com/microso
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
|
||||
1. A Microsoft Foundry project with a deployed model (e.g., `gpt-5.4`)
|
||||
2. Azure CLI installed and authenticated (`az login`)
|
||||
|
||||
## Environment Variables
|
||||
@@ -40,7 +40,7 @@ Before running this sample, ensure you have:
|
||||
Set the following environment variables:
|
||||
|
||||
```bash
|
||||
# Required: Your Azure AI Foundry project endpoint
|
||||
# Required: Your Microsoft Foundry project endpoint
|
||||
export AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
|
||||
|
||||
# Optional: Model deployment name (defaults to gpt-5.4)
|
||||
|
||||
@@ -31,7 +31,7 @@ public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -92,7 +92,7 @@ public static class Program
|
||||
string model)
|
||||
{
|
||||
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
$"{targetLanguage} Translator",
|
||||
$"{targetLanguage}Translator",
|
||||
new ProjectsAgentVersionCreationOptions(
|
||||
new DeclarativeAgentDefinition(model: model)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -34,7 +34,7 @@ public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -35,7 +35,7 @@ public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -37,7 +37,7 @@ public static class Program
|
||||
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -70,7 +70,7 @@ public static class Program
|
||||
|
||||
using var traceProvider = traceProviderBuilder.Build();
|
||||
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace WorkflowMagenticOrchestrationSample;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - An Azure AI Foundry project endpoint and model deployment must be configured.
|
||||
/// - A Microsoft Foundry project endpoint and model deployment must be configured.
|
||||
/// - Run <c>az login</c> before executing the sample.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
|
||||
@@ -15,7 +15,7 @@ This sample showcases the Magentic Orchestration Pattern in .NET, setting up a t
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT` set to your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_PROJECT_ENDPOINT` set to your Microsoft Foundry project endpoint
|
||||
- `FOUNDRY_MODEL` set to your model deployment name (defaults to `gpt-5.4-mini`)
|
||||
- `az login` completed before running the sample
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ namespace WorkflowAgentsInWorkflowsSample;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - An Azure AI Foundry project endpoint and model must be configured.
|
||||
/// - A Microsoft Foundry project endpoint and model must be configured.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -16,13 +16,13 @@ namespace WorkflowAgentsInWorkflowsSample;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - An Azure AI Foundry project endpoint and model must be configured.
|
||||
/// - A Microsoft Foundry project endpoint and model must be configured.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure AI Foundry client.
|
||||
// Set up the Microsoft Foundry client.
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ namespace MixedWorkflowWithAgentsAndExecutors;
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Previous foundational samples should be completed first.
|
||||
/// - An Azure AI Foundry project endpoint and model must be configured.
|
||||
/// - A Microsoft Foundry project endpoint and model must be configured.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
@@ -40,7 +40,7 @@ public static class Program
|
||||
{
|
||||
Console.WriteLine("\n=== Mixed Workflow: Agents and Executors ===\n");
|
||||
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
@@ -47,7 +47,7 @@ public static class Program
|
||||
Console.WriteLine("\n=== Writer-Critic Iteration Workflow ===\n");
|
||||
Console.WriteLine($"Writer and Critic will iterate up to {MaxIterations} times until approval.\n");
|
||||
|
||||
// Set up the Azure AI Foundry client
|
||||
// Set up the Microsoft Foundry client
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ internal static class Pages
|
||||
</div>
|
||||
<div id="chat"></div>
|
||||
<form id="form">
|
||||
<input id="input" placeholder="Try: 'What time is it?' or 'Search docs for Azure AI Foundry'" autocomplete="off" autofocus />
|
||||
<input id="input" placeholder="Try: 'What time is it?' or 'Search docs for Microsoft Foundry'" autocomplete="off" autofocus />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
<div id="status"></div>
|
||||
|
||||
@@ -13,12 +13,12 @@ The WriterAgent is configured with HTTPS redirection so the Aspire DevUI integra
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
|
||||
- [Aspire CLI](https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling)
|
||||
- An Azure subscription with access to [Azure AI Foundry](https://learn.microsoft.com/azure/ai-studio/)
|
||||
- An Azure subscription with access to [Microsoft Foundry](https://learn.microsoft.com/azure/ai-studio/)
|
||||
- Azure CLI authenticated (`az login`)
|
||||
|
||||
## Azure AI Foundry configuration
|
||||
## Microsoft Foundry configuration
|
||||
|
||||
The sample requires an Azure AI Foundry resource with a deployed `gpt-4.1` model. You have two options:
|
||||
The sample requires a Microsoft Foundry resource with a deployed `gpt-4.1` model. You have two options:
|
||||
|
||||
### Option 1: Connect to an existing Foundry resource
|
||||
|
||||
@@ -54,7 +54,7 @@ Remove or comment out the `AsExisting` block in `DevUIIntegration.AppHost/Progra
|
||||
// foundry.AsExisting(existingFoundryName, existingFoundryResourceGroup);
|
||||
```
|
||||
|
||||
Aspire will provision a new Azure AI Foundry resource on startup. The DevUI resource uses `.WaitFor(foundry)` transitively through the agent services, so the frontend won't become available until provisioning completes. This can take several minutes on first run.
|
||||
Aspire will provision a new Microsoft Foundry resource on startup. The DevUI resource uses `.WaitFor(foundry)` transitively through the agent services, so the frontend won't become available until provisioning completes. This can take several minutes on first run.
|
||||
|
||||
You still need to fill in the `Azure` section of `appsettings.json` (subscription, location, etc.) so Aspire knows where to create the resource.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample evaluates a pre-existing Azure AI Foundry agent against a rubric evaluator
|
||||
// This sample evaluates a pre-existing Microsoft Foundry agent against a rubric evaluator
|
||||
// that was authored in the Foundry portal.
|
||||
//
|
||||
// Rubric evaluators are LLM-as-judge evaluators with custom scoring dimensions you define
|
||||
@@ -9,7 +9,7 @@
|
||||
// here by name and version.
|
||||
//
|
||||
// Prerequisites:
|
||||
// - An Azure AI Foundry project with a deployed model.
|
||||
// - A Microsoft Foundry project with a deployed model.
|
||||
// - A registered Foundry agent in that project (the rubric was created against this agent).
|
||||
// - A rubric evaluator already created in the Foundry portal.
|
||||
// - .env (or environment) populated with the FOUNDRY_* variables below.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Evaluation — Foundry Rubric
|
||||
|
||||
This sample evaluates a pre-existing Azure AI Foundry agent against a **rubric evaluator**
|
||||
This sample evaluates a pre-existing Microsoft Foundry agent against a **rubric evaluator**
|
||||
authored in the Foundry portal. Rubric evaluators are LLM-as-judge evaluators with custom
|
||||
scoring dimensions you define for your domain; agent-framework references them by name and
|
||||
version, mixes them with built-in evaluators, and exposes per-dimension scores you can gate
|
||||
@@ -20,7 +20,7 @@ CI on.
|
||||
|
||||
- .NET 10 SDK or later.
|
||||
- Azure CLI installed and authenticated (`az login`).
|
||||
- An Azure AI Foundry project with a deployed model.
|
||||
- A Microsoft Foundry project with a deployed model.
|
||||
- A registered Foundry agent in that project (the agent the rubric was created against).
|
||||
- A rubric evaluator created in the Foundry portal. Creating rubrics through the portal
|
||||
currently requires picking a Foundry agent as the generation context, so this
|
||||
|
||||
@@ -30,7 +30,7 @@ dotnet/samples/
|
||||
│ │ └── openai/ # OpenAI provider samples
|
||||
│ ├── AgentOpenTelemetry/ # OpenTelemetry integration
|
||||
│ ├── AgentSkills/ # Agent skills patterns
|
||||
│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Foundry)
|
||||
│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Valkey, Foundry, AgentMemory)
|
||||
│ ├── AgentWithRAG/ # RAG patterns (text, vector store, Foundry)
|
||||
│ ├── AGUI/ # AG-UI protocol samples
|
||||
│ ├── DeclarativeAgents/ # Declarative agent definitions
|
||||
|
||||
@@ -20,7 +20,7 @@ public static class AnthropicClientExtensions
|
||||
/// <summary>
|
||||
/// Creates a new AI agent using the specified model and options.
|
||||
/// </summary>
|
||||
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent..</param>
|
||||
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent.</param>
|
||||
/// <param name="model">The model to use for chat completions.</param>
|
||||
/// <param name="instructions">The instructions for the AI agent.</param>
|
||||
/// <param name="name">The name of the AI agent.</param>
|
||||
@@ -74,7 +74,7 @@ public static class AnthropicClientExtensions
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="IAnthropicClient"/> using the Anthropic Chat Completion API.
|
||||
/// </summary>
|
||||
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent..</param>
|
||||
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent.</param>
|
||||
/// <param name="options">Full set of options to configure the agent.</param>
|
||||
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Core.Models;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -38,8 +39,27 @@ internal static class ActivityProcessor
|
||||
private static ChatMessage CreateChatMessageFromActivity(IActivity activity, IEnumerable<AIContent> messageContent) =>
|
||||
new(ChatRole.Assistant, [.. messageContent])
|
||||
{
|
||||
AdditionalProperties = MapAdditionalProperties(activity),
|
||||
AuthorName = activity.From?.Name,
|
||||
CreatedAt = activity.Timestamp,
|
||||
MessageId = activity.Id,
|
||||
RawRepresentation = activity
|
||||
};
|
||||
|
||||
private static AdditionalPropertiesDictionary? MapAdditionalProperties(IActivity activity)
|
||||
{
|
||||
IDictionary<string, JsonElement>? properties = activity.Properties;
|
||||
if (properties is null || properties.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var additionalProperties = new AdditionalPropertiesDictionary();
|
||||
foreach (KeyValuePair<string, JsonElement> property in properties)
|
||||
{
|
||||
additionalProperties[property.Key] = property.Value;
|
||||
}
|
||||
|
||||
return additionalProperties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -98,14 +99,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
responseMessagesList.Add(message);
|
||||
}
|
||||
|
||||
// TODO: Review list of ChatResponse properties to ensure we set all availble values.
|
||||
// Setting ResponseId and MessageId end up being particularly important for streaming consumers
|
||||
// so that they can tell things like response boundaries.
|
||||
return new AgentResponse(responseMessagesList)
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = responseMessagesList.LastOrDefault()?.MessageId,
|
||||
};
|
||||
return CreateAgentResponse(responseMessagesList, this.Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -132,24 +126,113 @@ public class CopilotStudioAgent : AIAgent
|
||||
string question = string.Join("\n", messages.Select(m => m.Text));
|
||||
var responseMessages = ActivityProcessor.ProcessActivityAsync(this.Client.AskQuestionAsync(question, typedSession.ConversationId, cancellationToken), streaming: true, this._logger);
|
||||
|
||||
// Enumerate the response messages
|
||||
await foreach (ChatMessage message in responseMessages.ConfigureAwait(false))
|
||||
await foreach (AgentResponseUpdate update in CreateAgentResponseUpdatesAsync(responseMessages, this.Id, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// TODO: Review list of ChatResponse properties to ensure we set all availble values.
|
||||
// Setting ResponseId and MessageId end up being particularly important for streaming consumers
|
||||
// so that they can tell things like response boundaries.
|
||||
yield return new AgentResponseUpdate(message.Role, message.Contents)
|
||||
{
|
||||
AgentId = this.Id,
|
||||
AdditionalProperties = message.AdditionalProperties,
|
||||
AuthorName = message.AuthorName,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
ResponseId = message.MessageId,
|
||||
MessageId = message.MessageId,
|
||||
};
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an <see cref="AgentResponse"/> from the messages returned by the Copilot Studio agent,
|
||||
/// populating the response-level metadata (such as <see cref="AgentResponse.CreatedAt"/>,
|
||||
/// <see cref="AgentResponse.FinishReason"/> and <see cref="AgentResponse.RawRepresentation"/>) from the
|
||||
/// final message so that consumers see the same surface as other <see cref="AIAgent"/> implementations.
|
||||
/// </summary>
|
||||
internal static AgentResponse CreateAgentResponse(IList<ChatMessage> messages, string? agentId)
|
||||
{
|
||||
ChatMessage? lastMessage = messages.Count > 0 ? messages[messages.Count - 1] : null;
|
||||
|
||||
return new AgentResponse(messages)
|
||||
{
|
||||
AgentId = agentId,
|
||||
ResponseId = lastMessage?.MessageId,
|
||||
CreatedAt = lastMessage?.CreatedAt,
|
||||
FinishReason = ChatFinishReason.Stop,
|
||||
RawRepresentation = lastMessage?.RawRepresentation,
|
||||
AdditionalProperties = lastMessage?.AdditionalProperties,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects the streamed <see cref="ChatMessage"/> sequence onto <see cref="AgentResponseUpdate"/> instances,
|
||||
/// carrying per-update metadata and setting <see cref="AgentResponseUpdate.FinishReason"/> only on the terminal
|
||||
/// update so streaming consumers can detect the response boundary.
|
||||
/// </summary>
|
||||
internal static async IAsyncEnumerable<AgentResponseUpdate> CreateAgentResponseUpdatesAsync(
|
||||
IAsyncEnumerable<ChatMessage> messages,
|
||||
string? agentId,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Buffer a single message so we know which update is the terminal one (it carries the finish reason).
|
||||
// Manual enumeration lets us still emit any already-received content if the source faults mid-stream,
|
||||
// preserving the original streaming behavior, before re-throwing the original exception.
|
||||
ChatMessage? pending = null;
|
||||
ExceptionDispatchInfo? failure = null;
|
||||
|
||||
IAsyncEnumerator<ChatMessage> enumerator = messages.GetAsyncEnumerator(cancellationToken);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
bool moved;
|
||||
try
|
||||
{
|
||||
moved = await enumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure = ExceptionDispatchInfo.Capture(ex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!moved)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (pending is not null)
|
||||
{
|
||||
yield return CreateAgentResponseUpdate(pending, agentId, finishReason: null);
|
||||
}
|
||||
|
||||
pending = enumerator.Current;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await enumerator.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch when (failure is not null)
|
||||
{
|
||||
// A fault was already captured from the stream; don't let a disposal
|
||||
// exception override the original streaming exception.
|
||||
}
|
||||
}
|
||||
|
||||
if (pending is not null)
|
||||
{
|
||||
// The last received message is the terminal update only when the stream completed successfully.
|
||||
yield return CreateAgentResponseUpdate(pending, agentId, finishReason: failure is null ? ChatFinishReason.Stop : null);
|
||||
}
|
||||
|
||||
failure?.Throw();
|
||||
}
|
||||
|
||||
private static AgentResponseUpdate CreateAgentResponseUpdate(ChatMessage message, string? agentId, ChatFinishReason? finishReason) =>
|
||||
new(message.Role, message.Contents)
|
||||
{
|
||||
AgentId = agentId,
|
||||
AdditionalProperties = message.AdditionalProperties,
|
||||
AuthorName = message.AuthorName,
|
||||
CreatedAt = message.CreatedAt,
|
||||
FinishReason = finishReason,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
ResponseId = message.MessageId,
|
||||
MessageId = message.MessageId,
|
||||
};
|
||||
|
||||
private async Task<string> StartNewConversationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string? conversationId = null;
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<PackageReference Include="Microsoft.Agents.CopilotStudio.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework Copilot Studio</Title>
|
||||
|
||||
@@ -607,7 +607,10 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
[Newtonsoft.Json.JsonProperty("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[Newtonsoft.Json.JsonProperty("ttl")]
|
||||
// Omit "ttl" from the document when null so Cosmos DB leaves TTL unset (disabled) instead of
|
||||
// rejecting the write. Cosmos requires ttl to be a positive integer or -1; a literal null is
|
||||
// invalid, so serializing MessageTtlSeconds = null must drop the property entirely.
|
||||
[Newtonsoft.Json.JsonProperty("ttl", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
|
||||
public int? Ttl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,7 +19,7 @@ using OpenAI.Evals;
|
||||
namespace Microsoft.Agents.AI.Foundry;
|
||||
|
||||
/// <summary>
|
||||
/// Azure AI Foundry evaluator provider that calls the Foundry Evals API.
|
||||
/// Microsoft Foundry evaluator provider that calls the Foundry Evals API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -28,7 +28,7 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// (quality, safety, agent behavior, tool usage) are supported.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Results appear in the Azure AI Foundry portal with a report URL for detailed analysis.
|
||||
/// Results appear in the Microsoft Foundry portal with a report URL for detailed analysis.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Serializing Dictionary<string, object> for eval API payloads.")]
|
||||
@@ -55,7 +55,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FoundryEvals"/> class.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="evaluators">
|
||||
/// Evaluator specs to use. Each entry can be a built-in evaluator name (string, for example
|
||||
@@ -80,7 +80,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with a conversation splitter.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="splitter">
|
||||
/// Default conversation splitter for multi-turn conversations.
|
||||
@@ -104,7 +104,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with full configuration.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="splitter">
|
||||
/// Default conversation splitter for multi-turn conversations.
|
||||
@@ -141,7 +141,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// Initializes a new instance of the <see cref="FoundryEvals"/> class using built-in evaluator
|
||||
/// names. Preserves source compatibility for callers that pass a <see cref="string"/> array.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="evaluators">Built-in evaluator names (for example <see cref="Relevance"/>).</param>
|
||||
public FoundryEvals(AIProjectClient projectClient, string model, string[] evaluators)
|
||||
@@ -153,7 +153,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with a splitter and
|
||||
/// built-in evaluator names.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="splitter">Default conversation splitter for multi-turn conversations.</param>
|
||||
/// <param name="evaluators">Built-in evaluator names.</param>
|
||||
@@ -170,7 +170,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with full configuration
|
||||
/// and built-in evaluator names.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="splitter">Default conversation splitter for multi-turn conversations.</param>
|
||||
/// <param name="pollIntervalSeconds">Seconds between status polls.</param>
|
||||
@@ -355,7 +355,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// Source-compat overload of <see cref="EvaluateTracesAsync(AIProjectClient, string, IEnumerable{string}, IEnumerable{string}, string, int, FoundryEvaluatorSpec[], string, double, double, CancellationToken)"/>
|
||||
/// that accepts a <see cref="string"/> array of built-in evaluator names.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="responseIds">Evaluate specific Responses API response IDs.</param>
|
||||
/// <param name="traceIds">Evaluate specific OTel trace IDs from App Insights.</param>
|
||||
@@ -403,7 +403,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// <paramref name="lookbackHours"/> to evaluate recent activity.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="responseIds">Evaluate specific Responses API response IDs.</param>
|
||||
/// <param name="traceIds">Evaluate specific OTel trace IDs from App Insights.</param>
|
||||
@@ -560,7 +560,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// Source-compat overload of <see cref="EvaluateFoundryTargetAsync(AIProjectClient, string, IDictionary{string, object}, IEnumerable{string}, FoundryEvaluatorSpec[], string, double, double, CancellationToken)"/>
|
||||
/// that accepts a <see cref="string"/> array of built-in evaluator names.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="target">Target configuration (must include a "type" key).</param>
|
||||
/// <param name="testQueries">Queries for Foundry to send to the target.</param>
|
||||
@@ -598,7 +598,7 @@ public sealed class FoundryEvals : IAgentEvaluator
|
||||
/// Foundry invokes the target, captures the output, and evaluates it.
|
||||
/// Use this for scheduled evaluations, red teaming, and CI/CD quality gates.
|
||||
/// </remarks>
|
||||
/// <param name="projectClient">The Azure AI Foundry project client.</param>
|
||||
/// <param name="projectClient">The Microsoft Foundry project client.</param>
|
||||
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
|
||||
/// <param name="target">Target configuration (must include a "type" key, e.g. "azure_ai_agent").</param>
|
||||
/// <param name="testQueries">Queries for Foundry to send to the target.</param>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for creating a <see cref="HarnessAgent"/> from an <see cref="IChatClient"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public static class ChatClientHarnessExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
#if NET
|
||||
using Microsoft.Agents.AI.Tools.Shell;
|
||||
#endif
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -43,15 +38,14 @@ namespace Microsoft.Agents.AI;
|
||||
/// <item><description><see cref="TodoProvider"/> — persistent todo list that the agent uses to track multi-step plans. Disable with <see cref="HarnessAgentOptions.DisableTodoProvider"/>.</description></item>
|
||||
/// <item><description><see cref="AgentModeProvider"/> — mode tracking (e.g., "plan" vs "execute") that the agent uses to structure its work. Disable with <see cref="HarnessAgentOptions.DisableAgentModeProvider"/>.</description></item>
|
||||
/// <item><description><see cref="FileMemoryProvider"/> — file-based session memory allowing the agent to persist notes and artifacts across turns. Disable with <see cref="HarnessAgentOptions.DisableFileMemory"/>.</description></item>
|
||||
/// <item><description><see cref="FileAccessProvider"/> — shared file access providing read/write tools for a working directory. Disable with <see cref="HarnessAgentOptions.DisableFileAccess"/>.</description></item>
|
||||
/// <item><description><see cref="AgentSkillsProvider"/> — discovers and loads skill definitions from the file system, enabling dynamic tool sets. Disable with <see cref="HarnessAgentOptions.DisableAgentSkillsProvider"/>.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Optional context providers (enabled via <see cref="HarnessAgentOptions"/>):</strong>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="FileAccessProvider"/> — shared file access providing read/write tools for a working directory. Enable by setting <see cref="HarnessAgentOptions.FileAccessStore"/>; configure via <see cref="HarnessAgentOptions.FileAccessProviderOptions"/>.</description></item>
|
||||
/// <item><description><see cref="BackgroundAgentsProvider"/> — enables delegation to background agents for parallel work. Enable by setting <see cref="HarnessAgentOptions.BackgroundAgents"/>.</description></item>
|
||||
/// <item><description><c>ShellEnvironmentProvider</c> — injects OS/shell/CWD information and a shell execution tool. Enable by setting <c>HarnessAgentOptions.ShellExecutor</c> (.NET only).</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -80,7 +74,6 @@ namespace Microsoft.Agents.AI;
|
||||
/// and combined with agent-specific instructions via <see cref="ChatOptions.Instructions"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class HarnessAgent : DelegatingAIAgent
|
||||
{
|
||||
/// <summary>
|
||||
@@ -222,6 +215,15 @@ public sealed class HarnessAgent : DelegatingAIAgent
|
||||
// Build ChatClient stack
|
||||
ChatClientBuilder chatClientBuilder = chatClient.AsBuilder();
|
||||
|
||||
// Registered first so it sits as the outermost decorator, above the approval-not-required bypassing
|
||||
// and function invocation middleware, so it can bind inbound approval responses to the requests the
|
||||
// framework surfaced. The harness uses UseProvidedChatClientAsIs, so this is added manually here rather
|
||||
// than via the default ChatClientAgent pipeline.
|
||||
if (options?.DisableApprovalResponseBinding is not true)
|
||||
{
|
||||
chatClientBuilder.UseApprovalResponseBinding();
|
||||
}
|
||||
|
||||
if (options?.DisableApprovalNotRequiredFunctionBypassing is not true)
|
||||
{
|
||||
chatClientBuilder.UseApprovalNotRequiredFunctionBypassing();
|
||||
@@ -279,16 +281,6 @@ public sealed class HarnessAgent : DelegatingAIAgent
|
||||
result.Tools.Add(new HostedWebSearchTool());
|
||||
}
|
||||
|
||||
#if NET
|
||||
if (options?.ShellExecutor is ShellExecutor shellExecutor)
|
||||
{
|
||||
result.Tools ??= [];
|
||||
result.Tools.Add(options.ShellToolName is { } shellToolName
|
||||
? shellExecutor.AsAIFunction(shellToolName, options.ShellToolDescription, !options.DisableShellToolApproval)
|
||||
: shellExecutor.AsAIFunction(description: options.ShellToolDescription, requireApproval: !options.DisableShellToolApproval));
|
||||
}
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -320,13 +312,9 @@ public sealed class HarnessAgent : DelegatingAIAgent
|
||||
}));
|
||||
}
|
||||
|
||||
if (options?.DisableFileAccess is not true)
|
||||
if (options?.FileAccessStore is AgentFileStore fileAccessStore)
|
||||
{
|
||||
AgentFileStore fileAccessStore = options?.FileAccessStore
|
||||
?? new FileSystemAgentFileStore(
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "working"));
|
||||
|
||||
providers.Add(new FileAccessProvider(fileAccessStore));
|
||||
providers.Add(new FileAccessProvider(fileAccessStore, options.FileAccessProviderOptions));
|
||||
}
|
||||
|
||||
if (options?.DisableAgentSkillsProvider is not true)
|
||||
@@ -347,13 +335,6 @@ public sealed class HarnessAgent : DelegatingAIAgent
|
||||
}
|
||||
}
|
||||
|
||||
#if NET
|
||||
if (options?.ShellExecutor is ShellExecutor shellExecutor)
|
||||
{
|
||||
providers.Add(new ShellEnvironmentProvider(shellExecutor, options.ShellEnvironmentProviderOptions));
|
||||
}
|
||||
#endif
|
||||
|
||||
if (options?.AIContextProviders is IEnumerable<AIContextProvider> userProviders)
|
||||
{
|
||||
providers.AddRange(userProviders);
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
#if NET
|
||||
using Microsoft.Agents.AI.Tools.Shell;
|
||||
#endif
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
@@ -14,7 +11,6 @@ namespace Microsoft.Agents.AI;
|
||||
/// <summary>
|
||||
/// Represents configuration options for a <see cref="HarnessAgent"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class HarnessAgentOptions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -46,6 +42,7 @@ public sealed class HarnessAgentOptions
|
||||
/// <see langword="true"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public int? MaxContextWindowTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -62,6 +59,7 @@ public sealed class HarnessAgentOptions
|
||||
/// is provided and <see cref="DisableCompaction"/> is <see langword="false"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public int? MaxOutputTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -81,6 +79,7 @@ public sealed class HarnessAgentOptions
|
||||
/// This property is ignored when <see cref="DisableCompaction"/> is <see langword="true"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public CompactionStrategy? CompactionStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -92,6 +91,7 @@ public sealed class HarnessAgentOptions
|
||||
/// <see cref="CompactionProvider"/> is added to the chat client pipeline, and the default
|
||||
/// <see cref="InMemoryChatHistoryProvider"/> is configured without a chat reducer.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public bool DisableCompaction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -162,6 +162,7 @@ public sealed class HarnessAgentOptions
|
||||
/// as a single-shot agent.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public IEnumerable<LoopEvaluator>? LoopEvaluators { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -171,6 +172,7 @@ public sealed class HarnessAgentOptions
|
||||
/// When <see langword="null"/>, the <see cref="LoopAgent"/> uses its default settings. This property is ignored
|
||||
/// when <see cref="LoopEvaluators"/> is <see langword="null"/> or empty.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public LoopAgentOptions? LoopAgentOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -216,6 +218,19 @@ public sealed class HarnessAgentOptions
|
||||
/// </remarks>
|
||||
public bool DisableApprovalNotRequiredFunctionBypassing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether binding inbound tool-approval responses to the
|
||||
/// model-originated approval requests that the framework surfaced is disabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="false"/> (the default), the underlying chat client pipeline includes the decorator
|
||||
/// added by <see cref="ChatClientBuilderExtensions.UseApprovalResponseBinding"/> as the outermost decorator
|
||||
/// above the function invocation middleware. It records each surfaced approval request and, on the next
|
||||
/// request, binds every approval response to its recorded request so an approved call matches exactly what
|
||||
/// was surfaced for approval.
|
||||
/// </remarks>
|
||||
public bool DisableApprovalResponseBinding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the <see cref="FileMemoryProvider"/> is disabled.
|
||||
/// </summary>
|
||||
@@ -234,27 +249,30 @@ public sealed class HarnessAgentOptions
|
||||
/// a default <see cref="FileSystemAgentFileStore"/> is created.
|
||||
/// This property is ignored when <see cref="DisableFileMemory"/> is <see langword="true"/>.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public AgentFileStore? FileMemoryStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the <see cref="FileAccessProvider"/> is disabled.
|
||||
/// Gets or sets the <see cref="AgentFileStore"/> that enables the <see cref="FileAccessProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="false"/> (the default), a <see cref="FileAccessProvider"/> is included in the
|
||||
/// agent's context providers, using either <see cref="FileAccessStore"/> or a default
|
||||
/// <see cref="FileSystemAgentFileStore"/> rooted at <c>{cwd}/working</c>.
|
||||
/// File access is opt-in. When <see langword="null"/> (the default), no <see cref="FileAccessProvider"/>
|
||||
/// is added and the agent has no file access tools. When set, a <see cref="FileAccessProvider"/> is
|
||||
/// included in the agent's context providers, backed by the supplied store and configured with
|
||||
/// <see cref="FileAccessProviderOptions"/> when provided.
|
||||
/// </remarks>
|
||||
public bool DisableFileAccess { get; set; }
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public AgentFileStore? FileAccessStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a custom <see cref="AgentFileStore"/> for the <see cref="FileAccessProvider"/>.
|
||||
/// Gets or sets the <see cref="FileAccessProviderOptions"/> used to configure the <see cref="FileAccessProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/> and <see cref="DisableFileAccess"/> is <see langword="false"/>,
|
||||
/// a default <see cref="FileSystemAgentFileStore"/> is created.
|
||||
/// This property is ignored when <see cref="DisableFileAccess"/> is <see langword="true"/>.
|
||||
/// This property is only used when <see cref="FileAccessStore"/> is set (file access is opt-in).
|
||||
/// When <see langword="null"/>, the provider uses its default options.
|
||||
/// </remarks>
|
||||
public AgentFileStore? FileAccessStore { get; set; }
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public FileAccessProviderOptions? FileAccessProviderOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the <see cref="HostedWebSearchTool"/> is disabled.
|
||||
@@ -348,6 +366,7 @@ public sealed class HarnessAgentOptions
|
||||
/// (case-insensitive). If these requirements are not met, <see cref="BackgroundAgentsProvider"/> will throw
|
||||
/// an <see cref="System.ArgumentException"/> during construction.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public IEnumerable<AIAgent>? BackgroundAgents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -357,67 +376,6 @@ public sealed class HarnessAgentOptions
|
||||
/// Use this to customize instructions or agent list formatting for the background agents feature.
|
||||
/// This property is ignored when <see cref="BackgroundAgents"/> is <see langword="null"/> or empty.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public BackgroundAgentsProviderOptions? BackgroundAgentsProviderOptions { get; set; }
|
||||
|
||||
#if NET
|
||||
/// <summary>
|
||||
/// Gets or sets the shell executor used to enable shell tool and environment probing via <see cref="ShellEnvironmentProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When non-null, a <see cref="ShellEnvironmentProvider"/> is automatically included in the agent's context
|
||||
/// providers (injecting OS/shell/CWD information into the system prompt), and the executor's
|
||||
/// <see cref="ShellExecutor.AsAIFunction"/> is registered as a callable tool.
|
||||
/// When <see langword="null"/> (the default), no shell features are enabled.
|
||||
/// </remarks>
|
||||
public ShellExecutor? ShellExecutor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the shell execution tool exposed to the model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/> (the default), the shell executor's default tool name (<c>run_shell</c>) is used.
|
||||
/// This property is ignored when <see cref="ShellExecutor"/> is <see langword="null"/>.
|
||||
/// </remarks>
|
||||
public string? ShellToolName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the shell execution tool shown to the model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/> (the default), the shell executor's built-in description is used.
|
||||
/// This property is ignored when <see cref="ShellExecutor"/> is <see langword="null"/>.
|
||||
/// </remarks>
|
||||
public string? ShellToolDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether approval is disabled for the shell execution tool.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When <see langword="false"/> (the default), the shell tool is wrapped in an <see cref="ApprovalRequiredAIFunction"/>
|
||||
/// so every command requires explicit approval before executing. When <see langword="true"/>, the tool can be invoked
|
||||
/// without approval. This property is ignored when <see cref="ShellExecutor"/> is <see langword="null"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Setting this to <see langword="true"/> also requires the underlying <see cref="ShellExecutor"/> to permit
|
||||
/// unapproved use. The inverse of this value is forwarded as the <c>requireApproval</c> argument to
|
||||
/// <see cref="ShellExecutor.AsAIFunction"/>, and some executors enforce their own security boundary:
|
||||
/// <see cref="LocalShellExecutor"/> throws an <see cref="System.InvalidOperationException"/> unless it was
|
||||
/// constructed with <see cref="LocalShellExecutorOptions.AcknowledgeUnsafe"/> set to <see langword="true"/>,
|
||||
/// because running unapproved commands directly on the host is inherently unsafe. Sandboxed executors such as
|
||||
/// <see cref="DockerShellExecutor"/> impose no such requirement.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool DisableShellToolApproval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional configuration for the <see cref="ShellEnvironmentProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this to customize which tools are probed, the probe timeout, shell family override,
|
||||
/// or the instructions formatter.
|
||||
/// This property is ignored when <see cref="ShellExecutor"/> is <see langword="null"/>.
|
||||
/// </remarks>
|
||||
public ShellEnvironmentProviderOptions? ShellEnvironmentProviderOptions { get; set; }
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsReleaseCandidate>false</IsReleaseCandidate>
|
||||
<IsReleased>true</IsReleased>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<NoWarn>$(NoWarn);MAAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<!-- Disable package validation baseline until the first release -->
|
||||
<PropertyGroup>
|
||||
<PackageValidationBaselineVersion />
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Tools.Shell\Microsoft.Agents.AI.Tools.Shell.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework Harness</Title>
|
||||
|
||||
@@ -271,6 +271,7 @@ class _CodeValidator(ast.NodeVisitor):
|
||||
self._allowed_builtins = allowed_builtins if allowed_builtins is not None else ALLOWED_BUILTINS
|
||||
self._blocked_builtins = blocked_builtins if blocked_builtins is not None else BLOCKED_BUILTINS
|
||||
self._allowed_os_attrs = allowed_os_attrs if allowed_os_attrs is not None else ALLOWED_OS_ATTRS
|
||||
self._os_aliases: set[str] = {"os"}
|
||||
|
||||
def validate(self, code: str) -> None:
|
||||
"""Validate code and raise CodeValidationError if it violates policy."""
|
||||
@@ -280,6 +281,7 @@ class _CodeValidator(ast.NodeVisitor):
|
||||
raise CodeValidationError(f"Syntax error in generated code: {exc}") from exc
|
||||
|
||||
self._errors = []
|
||||
self._os_aliases = {"os"}
|
||||
self.visit(tree)
|
||||
|
||||
if self._errors:
|
||||
@@ -303,6 +305,10 @@ class _CodeValidator(ast.NodeVisitor):
|
||||
self._errors.append(f"Import of '{alias_node.name}' is not allowed (blocked: {module_name})")
|
||||
elif module_name not in self._allowed_imports:
|
||||
self._errors.append(f"Import of '{alias_node.name}' is not allowed (not in allow-list)")
|
||||
if alias_node.name == "os":
|
||||
self._os_aliases.add(alias_node.asname or "os")
|
||||
elif alias_node.name.startswith("os.") and alias_node.asname is None:
|
||||
self._os_aliases.add("os")
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
@@ -324,6 +330,32 @@ class _CodeValidator(ast.NodeVisitor):
|
||||
self._errors.append(f"Import from 'os' of '{alias_node.name}' is not allowed")
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Assign(self, node: ast.Assign) -> None:
|
||||
"""Track re-bindings of the ``os`` module."""
|
||||
for target in node.targets:
|
||||
self._track_os_alias_targets(target, node.value)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
|
||||
"""Track annotated re-bindings of the ``os`` module."""
|
||||
if (
|
||||
isinstance(node.value, ast.Name)
|
||||
and node.value.id in self._os_aliases
|
||||
and isinstance(node.target, ast.Name)
|
||||
):
|
||||
self._os_aliases.add(node.target.id)
|
||||
self.generic_visit(node)
|
||||
|
||||
def _track_os_alias_targets(self, target: ast.AST, value: ast.AST) -> None:
|
||||
if isinstance(target, ast.Starred):
|
||||
target = target.value
|
||||
|
||||
if isinstance(target, ast.Name) and isinstance(value, ast.Name) and value.id in self._os_aliases:
|
||||
self._os_aliases.add(target.id)
|
||||
elif isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (ast.Tuple, ast.List)):
|
||||
for target_item, value_item in zip(target.elts, value.elts):
|
||||
self._track_os_alias_targets(target_item, value_item)
|
||||
|
||||
def visit_Call(self, node: ast.Call) -> None:
|
||||
"""Validate function calls.
|
||||
|
||||
@@ -357,7 +389,7 @@ class _CodeValidator(ast.NodeVisitor):
|
||||
# Enforce the `os` attribute allow-list. Anything outside `ALLOWED_OS_ATTRS`
|
||||
# (file I/O, process control, mutating helpers, etc.) is rejected so the
|
||||
# validator matches the documented `os.environ` / `os.path`-only contract.
|
||||
if isinstance(node.value, ast.Name) and node.value.id == "os" and node.attr not in self._allowed_os_attrs:
|
||||
if isinstance(node.value, ast.Name) and node.value.id in self._os_aliases and node.attr not in self._allowed_os_attrs:
|
||||
self._errors.append(f"Access to os.{node.attr} is not allowed")
|
||||
|
||||
# Block access to certain dangerous attributes
|
||||
|
||||
@@ -17,7 +17,7 @@ internal interface IScopedContentProcessor
|
||||
/// Process a list of messages.
|
||||
/// The list of messages should be a prompt or response.
|
||||
/// </summary>
|
||||
/// <param name="messages">A list of <see cref="ChatMessage"/> objects sent to the agent or received from the agent..</param>
|
||||
/// <param name="messages">A list of <see cref="ChatMessage"/> objects sent to the agent or received from the agent.</param>
|
||||
/// <param name="sessionId">The session where the messages were sent.</param>
|
||||
/// <param name="activity">An activity to indicate prompt or response.</param>
|
||||
/// <param name="purviewSettings">Purview settings containing tenant id, app name, etc.</param>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user