Skip to content

Commit 9bb040a

Browse files
ferantiverockittel
andauthored
feat (website): [refresh] port ChatUI from basic and use new Foundry (new) API (#86)
* migrate from Foundry (classic) to Foundry (new) projects API * update zip * update infra from net8 to net10 * update foundry agents URL/request body from classic to Microsoft Foundry (new) * update README for Foundry (new) * change how to replace json field values * add new required rbac * remove not in use sln file * disabled public network access that isn’t supported in the new Foundry portal experience yet * change zip file github repo name * add new required rbac * modify rbac since colls are getting created later in the process * fix iaac agent field name * Address PR Feedback: remove unused vars * fix heads numbering * Address PR Feedback: add placeholder to reinstate optional test when private network access is supported * Address PR Feedback: fix url inconsistency * Address PR Feedback: fix grammar issue * Address PR Feedback: add comment for diagnostic suppression * Address PR Feedback: fix typo * improve a bit network isolation limitation placeholder highlights: change this to be agent-oriented instead of project while those are affected as well but out of scope for this teaching moment in this baseline * Revert "fix heads numbering" This reverts commit 84dfbad. Co-authored-by: Chad Kittel <chad.kittel@gmail.com> * Revert "disabled public network access that isn’t supported in the new Foundry portal experience yet" This reverts commit 2482ea9. Co-authored-by: Chad Kittel <chad.kittel@gmail.com> * Address PR Feedback: add intermeditate note and improve warn text Co-authored-by: Chad Kittel <chad.kittel@gmail.com> --------- Co-authored-by: Chad Kittel <chad.kittel@gmail.com>
1 parent 87201dd commit 9bb040a

File tree

11 files changed

+92
-150
lines changed

11 files changed

+92
-150
lines changed

.devcontainer/devcontainer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Foundry Agent Service chat baseline reference implementation",
3-
"image": "mcr.microsoft.com/devcontainers/dotnet:dev-8.0-jammy",
3+
"image": "mcr.microsoft.com/devcontainers/dotnet:dev-10.0",
44
"runArgs": ["--network=host"],
55
"remoteUser": "vscode",
66
"features": {
@@ -13,7 +13,7 @@
1313
"ms-azuretools.vscode-bicep"
1414
],
1515
"settings": {}
16-
}
16+
}
1717
},
1818
"forwardPorts": []
19-
}
19+
}

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ The AI agent definition would likely be deployed from your application's pipelin
225225
$FOUNDRY_PROJECT_NAME="projchat"
226226
$MODEL_CONNECTION_NAME="agent-model"
227227
$BING_CONNECTION_ID="$(az cognitiveservices account show -n $FOUNDRY_NAME -g $RESOURCE_GROUP --query 'id' --out tsv)/projects/${FOUNDRY_PROJECT_NAME}/connections/${BING_CONNECTION_NAME}"
228-
$FOUNDRY_AGENT_CREATE_URL="https://${FOUNDRY_NAME}.services.ai.azure.com/api/projects/${FOUNDRY_PROJECT_NAME}/assistants?api-version=2025-05-15-preview"
228+
$FOUNDRY_AGENT_CREATE_URL="https://${FOUNDRY_NAME}.services.ai.azure.com/api/projects/${FOUNDRY_PROJECT_NAME}/agents?api-version=2025-11-15-preview"
229229
230230
echo $BING_CONNECTION_ID
231231
echo $MODEL_CONNECTION_NAME
@@ -238,22 +238,30 @@ The AI agent definition would likely be deployed from your application's pipelin
238238

239239
```powershell
240240
# Use the agent definition on disk
241-
Invoke-WebRequest -Uri "https://github.com/Azure-Samples/azure-ai-foundry-baseline/raw/refs/heads/main/agents/chat-with-bing.json" -OutFile "chat-with-bing.json"
241+
Invoke-WebRequest -Uri "https://github.com/Azure-Samples/microsoft-foundry-baseline/raw/refs/heads/main/agents/chat-with-bing.json" -OutFile "chat-with-bing.json"
242242
243243
# Update to match your environment
244-
${c:chat-with-bing-output.json} = ${c:chat-with-bing.json} -replace 'MODEL_CONNECTION_NAME', $MODEL_CONNECTION_NAME -replace 'BING_CONNECTION_ID', $BING_CONNECTION_ID
244+
$chat_agent = Get-Content .\chat-with-bing.json -Raw | ConvertFrom-Json
245+
246+
$chat_agent.definition.model = $MODEL_CONNECTION_NAME
247+
$chat_agent.definition.tools[0].bing_grounding.search_configurations[0].project_connection_id = $BING_CONNECTION_ID
248+
249+
$chat_agent | ConvertTo-Json -Depth 10 | Set-Content .\chat-with-bing-output.json
245250
246251
# Deploy the agent
247252
az rest -u $FOUNDRY_AGENT_CREATE_URL -m "post" --resource "https://ai.azure.com" -b @chat-with-bing-output.json
248253
249254
# Capture the Agent's ID
250-
$AGENT_ID="$(az rest -u $FOUNDRY_AGENT_CREATE_URL -m 'get' --resource 'https://ai.azure.com' --query 'data[0].id' -o tsv)"
255+
$AGENT_ID="$(az rest -u $FOUNDRY_AGENT_CREATE_URL -m 'get' --resource 'https://ai.azure.com' --query last_id -o tsv)"
251256
252257
echo $AGENT_ID
253258
```
254259

255260
### 3. Test the agent from the Foundry portal in the playground. *Optional.*
256261

262+
| :warning: | The new Foundry portal experience does not currently support the end-to-end network isolation used in this architecture. Using this secured architecture, you will only be able to create and call your agents through the SDK or REST API; not interface with them in the Foundry portal. See, [How to use a virtual network with the Foundry Agent Service](https://learn.microsoft.com/azure/ai-foundry/agents/how-to/virtual-networks?view=foundry&preserve-view=true). These intermediate testing instructions will be updated when this experience is supported. |
263+
| :-------: | :------------------------- |
264+
257265
Here you'll test your orchestration agent by invoking it directly from the Foundry portal's playground experience. The Foundry portal is only accessible from your private network, so you'll do this from your jump box.
258266
259267
*This step testing step is completely optional.*
@@ -262,17 +270,17 @@ Here you'll test your orchestration agent by invoking it directly from the Found
262270
263271
You'll need to sign in to the Azure portal, and resolve any Entra ID Conditional Access policies on your account, if this is the first time you are connecting through the jump box.
264272

265-
1. Navigate to the Foundry project named **projchat** in your resource group and open the Foundry portal by clicking the **Go to Microsoft Foundry portal** button.
273+
1. Navigate to the Foundry project named **projchat** in your resource group and open the Foundry portal by clicking the **Go to Foundry portal** button.
266274

267275
This will take you directly into the 'Chat project'. Alternatively, you can find all your Foundry accounts and projects by going to <https://ai.azure.com> and you do not need to use the Azure portal to access them.
268276

269-
1. Click **Agents** in the side navigation.
277+
1. In the upper-right corner, if not already enabled, toggle New Foundry to switch into the Microsoft Foundry (new) portal.
270278

271-
1. Select the agent named 'Baseline Chatbot Agent'.
279+
1. In the top-right corner, click Build. This opens by default the Agents blade in the side navigation, where you can view the available agents and create new ones.
272280

273-
1. Click the **Try in playground** button.
281+
1. Select the agent you just created from the previous step named 'baseline-chatbot-agent'.
274282

275-
1. Enter a question that would require grounding data through recent internet content, such as a notable recent event or the weather today in your location.
283+
1. Enter a question to the agent that would require grounding data through recent internet content, such as a notable recent event or the weather today in your location.
276284

277285
1. A grounded response to your question should appear on the UI.
278286

@@ -291,7 +299,7 @@ For this deployment guide, you'll continue using your jump box to simulate part
291299
1. Using the same PowerShell terminal session from previous steps, download the web UI.
292300
293301
```powershell
294-
Invoke-WebRequest -Uri https://github.com/Azure-Samples/azure-ai-foundry-baseline/raw/refs/heads/main/website/chatui.zip -OutFile chatui.zip
302+
Invoke-WebRequest -Uri https://github.com/Azure-Samples/microsoft-foundry-baseline/raw/refs/heads/main/website/chatui.zip -OutFile chatui.zip
295303
```
296304
297305
1. Upload the web application to Azure Storage, where the web app will load the code from.
@@ -394,5 +402,4 @@ Please see our [Contributor guide](./CONTRIBUTING.md).
394402
395403
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact <opencode@microsoft.com> with any additional questions or comments.
396404
397-
With :heart: from Azure Patterns & Practices, [Azure Architecture Center](https://azure.com/architecture).
398-
405+
With :heart: from Azure Patterns & Practices, [Azure Architecture Center](https://azure.com/architecture).

agents/chat-with-bing.json

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
{
2-
"name": "Baseline Chatbot Agent",
3-
"description": "Example of an Foundry Agent that uses the Bing Search tool to answer questions. Used in the Microsoft Learn AI chat reference architecture. https://learn.microsoft.com/azure/architecture/architecture/baseline-azure-ai-foundry-chat",
4-
"model": "MODEL_CONNECTION_NAME",
5-
"instructions": "You are a helpful Chatbot agent. You'll consult the Bing Search tool to answer questions. Always search the web for information before responding.",
6-
"tools": [
7-
{
8-
"type": "bing_grounding",
9-
"bing_grounding": {
10-
"search_configurations": [
11-
{
12-
"connection_id": "BING_CONNECTION_ID",
13-
"count": 5,
14-
"freshness": "Week"
15-
}
16-
]
2+
"name": "baseline-chatbot-agent",
3+
"description": "Example of a Foundry Agent that uses the Bing Search tool to answer questions. Used in the Microsoft Learn AI chat reference architecture. https://learn.microsoft.com/azure/architecture/architecture/baseline-azure-ai-foundry-chat",
4+
"definition": {
5+
"kind": "prompt",
6+
"model": "MODEL_CONNECTION_NAME",
7+
"instructions": "You are a helpful Chatbot agent. You'll consult the Bing Search tool to answer questions. Always search the web for information before responding.",
8+
"tools": [
9+
{
10+
"type": "bing_grounding",
11+
"bing_grounding": {
12+
"search_configurations": [
13+
{
14+
"project_connection_id": "BING_CONNECTION_ID",
15+
"count": 5,
16+
"freshness": "Week"
17+
}
18+
]
19+
}
1720
}
18-
}
19-
],
20-
"tool_resources": {},
21-
"temperature": 1,
22-
"top_p": 1,
21+
],
22+
"tool_resources": {},
23+
"temperature": 1,
24+
"top_p": 1
25+
},
2326
"metadata": {
2427
"createdBy": "Microsoft Learn Baseline Architecture",
2528
"version": "1.0.0"
26-
},
27-
"response_format": "auto"
29+
}
2830
}

azure-ai-foundry-baseline.sln

Lines changed: 0 additions & 29 deletions
This file was deleted.

infra-as-code/bicep/ai-foundry-project.bicep

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ resource agentUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentit
4444
var workspaceId = foundry::project.properties.internalId
4545
var workspaceIdAsGuid = '${substring(workspaceId, 0, 8)}-${substring(workspaceId, 8, 4)}-${substring(workspaceId, 12, 4)}-${substring(workspaceId, 16, 4)}-${substring(workspaceId, 20, 12)}'
4646

47-
var scopeUserContainerId = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDbAccount.name}/dbs/enterprise_memory/colls/${workspaceIdAsGuid}-thread-message-store'
48-
var scopeSystemContainerId = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDbAccount.name}/dbs/enterprise_memory/colls/${workspaceIdAsGuid}-system-thread-message-store'
49-
var scopeEntityContainerId = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDbAccount.name}/dbs/enterprise_memory/colls/${workspaceIdAsGuid}-agent-entity-store'
47+
var scopeAllContainers = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDbAccount.name}/dbs/enterprise_memory'
5048

5149
@description('Existing Azure Cosmos DB account. Will be assigning Data Contributor role to the Foundry project\'s identity.')
5250
resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = {
@@ -231,55 +229,21 @@ module projectBlobDataOwnerConditionalAssignment './modules/storageAccountRoleAs
231229

232230
// Sql Role Assignments
233231

234-
@description('Assign the project\'s managed identity the ability to read and write data in this collection within enterprise_memory database.')
235-
module projectUserThreadContainerWriterSqlAssignment './modules/cosmosdbSqlRoleAssignment.bicep' = {
236-
name: 'projectUserThreadContainerWriterSqlAssignmentDeploy'
232+
@description('Assign the project\'s managed identity the ability to read and write data in all collections within enterprise_memory database.')
233+
module containersWriterSqlAssignment './modules/cosmosdbSqlRoleAssignment.bicep' = {
234+
name: 'containersWriterSqlAssignmentDeploy'
237235
params: {
238236
roleDefinitionId: cosmosDbAccount::dataContributorRole.id
239237
principalId: agentUserManagedIdentity.properties.principalId
240238
existingCosmosDbAccountName: existingCosmosDbAccountName
241239
existingCosmosDbName: 'enterprise_memory'
242-
existingCosmosCollectionTypeName: 'user'
243-
scopeUserContainerId: scopeUserContainerId
240+
existingCosmosCollectionTypeName: 'containers'
241+
scopeUserContainerId: scopeAllContainers
244242
}
245243
dependsOn: [
246244
foundry::project::aiAgentService
247245
]
248246
}
249-
250-
@description('Assign the project\'s managed identity the ability to read and write data in this collection within enterprise_memory database.')
251-
module projectSystemThreadContainerWriterSqlAssignment './modules/cosmosdbSqlRoleAssignment.bicep' = {
252-
name: 'projectSystemThreadContainerWriterSqlAssignmentDeploy'
253-
params: {
254-
roleDefinitionId: cosmosDbAccount::dataContributorRole.id
255-
principalId: agentUserManagedIdentity.properties.principalId
256-
existingCosmosDbAccountName: existingCosmosDbAccountName
257-
existingCosmosDbName: 'enterprise_memory'
258-
existingCosmosCollectionTypeName: 'system'
259-
scopeUserContainerId: scopeSystemContainerId
260-
}
261-
dependsOn: [
262-
foundry::project::aiAgentService
263-
projectUserThreadContainerWriterSqlAssignment // Single thread applying these permissions.
264-
]
265-
}
266-
267-
@description('Assign the project\'s managed identity the ability to read and write data in this collection within enterprise_memory database.')
268-
module projectEntityContainerWriterSqlAssignment './modules/cosmosdbSqlRoleAssignment.bicep' = {
269-
name: 'projectEntityContainerWriterSqlAssignmentDeploy'
270-
params: {
271-
roleDefinitionId: cosmosDbAccount::dataContributorRole.id
272-
principalId: agentUserManagedIdentity.properties.principalId
273-
existingCosmosDbAccountName: existingCosmosDbAccountName
274-
existingCosmosDbName: 'enterprise_memory'
275-
existingCosmosCollectionTypeName: 'entities'
276-
scopeUserContainerId: scopeEntityContainerId
277-
}
278-
dependsOn: [
279-
foundry::project::aiAgentService
280-
projectSystemThreadContainerWriterSqlAssignment // Single thread applying these permissions.
281-
]
282-
}
283247

284248
// ---- Outputs ----
285249

infra-as-code/bicep/web-app.bicep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = {
201201
http20Enabled: false
202202
publicNetworkAccess: 'Disabled'
203203
alwaysOn: true
204-
linuxFxVersion: 'DOTNETCORE|8.0'
204+
linuxFxVersion: 'DOTNETCORE|10.0'
205205
netFrameworkVersion: null
206206
windowsFxVersion: null
207207
}

website/chatui.zip

1.89 MB
Binary file not shown.
Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using Microsoft.AspNetCore.Mvc;
22
using Microsoft.Extensions.Options;
3-
using Azure;
4-
using Azure.AI.Agents.Persistent;
3+
using Azure.AI.Projects;
4+
using Azure.AI.Projects.OpenAI;
5+
using OpenAI.Responses;
56
using chatui.Configuration;
67

78
namespace chatui.Controllers;
@@ -10,56 +11,52 @@ namespace chatui.Controllers;
1011
[Route("[controller]/[action]")]
1112

1213
public class ChatController(
13-
PersistentAgentsClient client,
14+
AIProjectClient projectClient,
1415
IOptionsMonitor<ChatApiOptions> options,
1516
ILogger<ChatController> logger) : ControllerBase
1617
{
17-
private readonly PersistentAgentsClient _client = client;
18+
private readonly AIProjectClient _projectClient = projectClient;
1819
private readonly IOptionsMonitor<ChatApiOptions> _options = options;
1920
private readonly ILogger<ChatController> _logger = logger;
2021

21-
// TODO: [security] Do not trust client to provide threadId. Instead map current user to their active threadid in your application's own state store.
22-
// Without this security control in place, a user can inject messages into another user's thread.
23-
[HttpPost("{threadId}")]
24-
public async Task<IActionResult> Completions([FromRoute] string threadId, [FromBody] string prompt)
22+
// TODO: [security] Do not trust client to provide conversationId. Instead map current user to their active conversationId in your application's own state store.
23+
// Without this security control in place, a user can inject messages into another user's conversation.
24+
[HttpPost("{conversationId}")]
25+
public async Task<IActionResult> Completions([FromRoute] string conversationId, [FromBody] string message)
2526
{
26-
if (string.IsNullOrWhiteSpace(prompt))
27-
throw new ArgumentException("Prompt cannot be null, empty, or whitespace.", nameof(prompt));
27+
if (string.IsNullOrWhiteSpace(message))
28+
throw new ArgumentException("Message cannot be null, empty, or whitespace.", nameof(message));
29+
_logger.LogDebug("Prompt received {Prompt}", message);
2830

29-
_logger.LogDebug("Prompt received {Prompt}", prompt);
30-
var _config = _options.CurrentValue;
31-
32-
PersistentThreadMessage message = await _client.Messages.CreateMessageAsync(
33-
threadId,
34-
MessageRole.User,
35-
prompt);
31+
// MessageResponseItem is currently intended for evaluation purposes and therefore requires explicit suppression of compiler diagnostics.
32+
#pragma warning disable OPENAI001
33+
MessageResponseItem userMessageResponseItem = ResponseItem.CreateUserMessageItem(
34+
[ResponseContentPart.CreateInputTextPart(message)]);
3635

37-
ThreadRun run = await _client.Runs.CreateRunAsync(threadId, _config.AIAgentId);
36+
var _config = _options.CurrentValue;
37+
AgentRecord agentRecord = await _projectClient.Agents.GetAgentAsync(_config.AIAgentId);
38+
var agent = agentRecord.Versions.Latest;
3839

39-
while (run.Status == RunStatus.Queued || run.Status == RunStatus.InProgress || run.Status == RunStatus.RequiresAction)
40-
{
41-
await Task.Delay(TimeSpan.FromMilliseconds(500));
42-
run = (await _client.Runs.GetRunAsync(threadId, run.Id)).Value;
43-
}
40+
ProjectResponsesClient responsesClient
41+
= _projectClient.OpenAI.GetProjectResponsesClientForAgent(agent, conversationId);
4442

45-
Pageable<PersistentThreadMessage> messages = _client.Messages.GetMessages(
46-
threadId: threadId, order: ListSortOrder.Ascending);
43+
var agentResponseItem = await responsesClient.CreateResponseAsync([userMessageResponseItem]);
4744

48-
var fullText =
49-
messages
50-
.Where(m => m.Role == MessageRole.Agent)
51-
.SelectMany(m => m.ContentItems.OfType<MessageTextContent>())
52-
.Last().Text;
45+
var fullText = agentResponseItem.Value.GetOutputText();
5346

5447
return Ok(new { data = fullText });
5548
}
5649

5750
[HttpPost]
58-
public async Task<IActionResult> Threads()
51+
public async Task<IActionResult> Conversations()
5952
{
60-
// TODO [performance efficiency] Delay creating a thread until the first user message arrives.
61-
PersistentAgentThread thread = await _client.Threads.CreateThreadAsync();
53+
// TODO [performance efficiency] Delay creating a conversation until the first user message arrives.
54+
ProjectConversationCreationOptions conversationOptions = new();
55+
56+
ProjectConversation conversation
57+
= await _projectClient.OpenAI.Conversations.CreateProjectConversationAsync(
58+
conversationOptions);
6259

63-
return Ok(new { id = thread.Id });
60+
return Ok(new { id = conversation.Id });
6461
}
6562
}

0 commit comments

Comments
 (0)