Skip to content

Commit 659feb3

Browse files
ferantiveroCopilot
andauthored
feat (website): [refact] migrate chatui from Foundry SDK to MAF Foundry provider (#76)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent eac7602 commit 659feb3

10 files changed

Lines changed: 78 additions & 58 deletions

File tree

README.md

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,22 @@ The following workflow corresponds to the previous diagram:
2626

2727
1. An application user interacts with a web application that contains chat functionality. They issue an HTTPS request to the App Service default domain on azurewebsites.net. This domain automatically points to the App Service built-in public IP address. The Transport Layer Security connection is established from the client directly to App Service. Azure fully manages the certificate.
2828
1. The App Service feature called Easy Auth ensures that the user who accesses the website is authenticated via Microsoft Entra ID.
29-
1. The application code deployed to App Service handles the request and renders a chat UI for the application user. The chat UI code connects to APIs that are also hosted in that same App Service instance. The API code connects to an agent in Microsoft Foundry by using the Azure AI Persistent Agents SDK.
29+
1. The application code deployed to App Service handles the request and renders a chat UI for the application user. The chat UI code connects to APIs that are also hosted in that same App Service instance. The API code connects to an agent in Microsoft Foundry through the [Microsoft Agent Framework](https://github.com/microsoft/agent-framework), which calls the [OpenAI Conversations](https://learn.microsoft.com/rest/api/aifoundry/aiproject#conversations) and [Responses](https://learn.microsoft.com/rest/api/aifoundry/aiproject#responses-94) APIs under the hood.
3030
1. Foundry Agent Service connects to Azure AI Search to fetch grounding data for the query. The grounding data is added to the prompt that's sent to the Azure OpenAI model in the next step.
3131
1. Foundry Agent Service connects to an Azure OpenAI model that's deployed in Foundry and sends the prompt that includes the relevant grounding data and chat context.
3232
1. Application Insights logs information about the original request to App Service and the call agent interactions.
3333

3434
### Deploying an agent into Foundry Agent Service
3535

36-
Agents can be created via the Foundry portal, [Azure AI Persistent Agents client library](https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent), or the [REST API](https://learn.microsoft.com/rest/api/aifoundry/aiagents/). The creation and invocation of agents are a data plane operation.
36+
Agents can be created via the Foundry portal, [Foundry SDK](https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Projects), or the [REST API](https://learn.microsoft.com/rest/api/aifoundry/aiagents/). The creation and invocation of agents are a data plane operation.
3737

3838
Ideally agents should be source-controlled and a versioned asset. You then can deploy agents in a coordinated way with the rest of your workload's code. In this deployment guide, you'll create an agent through the REST API to simulate a deployment pipeline which could have created the agent.
3939

4040
### Invoking the agent from .NET code hosted in an Azure Web App
4141

42-
A chat UI application is deployed into Azure App Service. The .NET code uses the [Azure AI Persistent Agents client library](https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent) to connect to the workload's agent. The endpoint for the agent is exposed over internet through Foundry.
42+
A chat UI application is deployed into Azure App Service. The .NET code uses the [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) with the Foundry provider to connect to the workload's agent. The endpoint for the agent is exposed over internet through Foundry.
43+
44+
Agent invocation at runtime uses a different API surface than agent lifecycle management. [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) uses exclusively the [OpenAI Conversations](https://learn.microsoft.com/rest/api/aifoundry/aiproject#conversations) and [Responses](https://learn.microsoft.com/rest/api/aifoundry/aiproject#responses-94) APIs, identifying the agent by name and version directly in the request body.
4345

4446
## Deployment guide
4547

@@ -164,37 +166,25 @@ Here you'll test your orchestration agent by invoking it directly from the Found
164166

165167
1. A grounded response to your question should appear on the UI.
166168

167-
### 4. Publish the chat front-end web app
169+
### 4. Deploy the chat front-end web app
168170

169171
Workloads build chat functionality into an application. Those interfaces usually call Foundry project endpoint invoking a particular agent. This implementation comes with such an interface. You'll deploy it to Azure App Service using the Azure CLI.
170172

171-
1. Generate some variables to set context.
172-
173-
*The following variables align with the defaults in this deployment. Update them if you customized anything.*
173+
1. Update the app configuration to use the agent you deployed.
174174

175175
```bash
176176
FOUNDRY_NAME="aif${BASE_NAME}"
177177
FOUNDRY_PROJECT_NAME="projchat"
178178
FOUNDRY_AGENTS_URL="https://${FOUNDRY_NAME}.services.ai.azure.com/api/projects/${FOUNDRY_PROJECT_NAME}/agents?api-version=2025-11-15-preview"
179179

180-
echo $FOUNDRY_AGENTS_URL
181-
```
182-
183-
1. Get Agent ID value.
184-
185-
```bash
186180
AGENT_ID=$(az rest -u $FOUNDRY_AGENTS_URL -m "get" --resource "https://ai.azure.com" --query last_id -o tsv)
181+
AGENT_VERSION=$(az rest -u $FOUNDRY_AGENTS_URL -m "get" --resource "https://ai.azure.com" --query 'data[-1].versions.latest.version' -o tsv)
187182

188-
echo $AGENT_ID
189-
````
190-
191-
1. Update the app configuration to use the agent you deployed.
183+
echo "$AGENT_ID (version $AGENT_VERSION)"
192184

193-
```bash
194185
APPSERVICE_NAME=app-$BASE_NAME
195-
196-
az webapp config appsettings set -g $RESOURCE_GROUP -n $APPSERVICE_NAME --settings AIAgentId=${AGENT_ID}
197-
````
186+
az webapp config appsettings set -g $RESOURCE_GROUP -n $APPSERVICE_NAME --settings AIAgentId=${AGENT_ID} AIAgentVersion=${AGENT_VERSION}
187+
```
198188

199189
1. Deploy the ChatUI web app
200190

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = {
142142
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
143143
AIProjectEndpoint: foundry::project.properties.endpoints['AI Foundry API']
144144
AIAgentId: 'Not yet set' // Will be set once the agent is created
145+
AIAgentVersion: 'Not yet set' // Will be set once the agent is created
145146
XDT_MicrosoftApplicationInsights_Mode: 'Recommended'
146147
}
147148
}

website/chatui.zip

100755100644
1.48 MB
Binary file not shown.

website/chatui/Configuration/ChatApiOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ public class ChatApiOptions
99

1010
[Required]
1111
public string AIAgentId { get; init; } = default!;
12+
13+
[Required]
14+
public string AIAgentVersion { get; init; } = default!;
1215
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Azure.AI.Projects;
2+
using Azure.AI.Extensions.OpenAI;
3+
using Microsoft.Agents.AI.Foundry;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace chatui.Configuration;
7+
8+
#pragma warning disable OPENAI001 // FoundryAgent is experimental
9+
10+
public class FoundryAgentResolver : IDisposable
11+
{
12+
private readonly AIProjectClient _projectClient;
13+
private readonly IOptionsMonitor<ChatApiOptions> _options;
14+
private readonly IDisposable? _changeToken;
15+
private volatile FoundryAgent? _agent;
16+
17+
public FoundryAgentResolver(AIProjectClient projectClient, IOptionsMonitor<ChatApiOptions> options)
18+
{
19+
_projectClient = projectClient;
20+
_options = options;
21+
_changeToken = options.OnChange(_ => Interlocked.Exchange(ref _agent, null));
22+
}
23+
24+
public FoundryAgent GetAgent()
25+
{
26+
var current = _agent;
27+
if (current is not null)
28+
return current;
29+
30+
var name = _options.CurrentValue.AIAgentId;
31+
var version = _options.CurrentValue.AIAgentVersion;
32+
var agent = _projectClient.AsAIAgent(new AgentReference(name, version));
33+
34+
_agent = agent;
35+
return agent;
36+
}
37+
38+
public void Dispose()
39+
{
40+
_changeToken?.Dispose();
41+
}
42+
}
Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,44 @@
11
using Microsoft.AspNetCore.Mvc;
2-
using Microsoft.Extensions.Options;
3-
using Azure.AI.Projects;
4-
using Azure.AI.Projects.OpenAI;
5-
using OpenAI.Responses;
2+
using Microsoft.Agents.AI;
3+
using Microsoft.Agents.AI.Foundry;
64
using chatui.Configuration;
75

86
namespace chatui.Controllers;
97

8+
#pragma warning disable OPENAI001 // FoundryAgent is experimental
9+
1010
[ApiController]
1111
[Route("[controller]/[action]")]
12-
1312
public class ChatController(
14-
AIProjectClient projectClient,
15-
IOptionsMonitor<ChatApiOptions> options,
13+
FoundryAgentResolver agentResolver,
1614
ILogger<ChatController> logger) : ControllerBase
1715
{
18-
private readonly AIProjectClient _projectClient = projectClient;
19-
private readonly IOptionsMonitor<ChatApiOptions> _options = options;
20-
private readonly ILogger<ChatController> _logger = logger;
21-
2216
// TODO: [security] Do not trust client to provide conversationId. Instead map current user to their active conversationId in your application's own state store.
2317
// Without this security control in place, a user can inject messages into another user's conversation.
2418
[HttpPost("{conversationId}")]
25-
public async Task<IActionResult> Completions([FromRoute] string conversationId, [FromBody] string message)
19+
public async Task<IActionResult> Responses([FromRoute] string conversationId, [FromBody] string message)
2620
{
2721
if (string.IsNullOrWhiteSpace(message))
2822
throw new ArgumentException("Message cannot be null, empty, or whitespace.", nameof(message));
29-
_logger.LogDebug("Prompt received {Prompt}", message);
30-
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)]);
35-
36-
var _config = _options.CurrentValue;
37-
AgentRecord agentRecord = await _projectClient.Agents.GetAgentAsync(_config.AIAgentId);
38-
var agent = agentRecord.Versions.Latest;
39-
40-
ProjectResponsesClient responsesClient
41-
= _projectClient.OpenAI.GetProjectResponsesClientForAgent(agent, conversationId);
23+
logger.LogDebug("Prompt received {Prompt}", message);
4224

43-
var agentResponseItem = await responsesClient.CreateResponseAsync([userMessageResponseItem]);
25+
FoundryAgent agent = agentResolver.GetAgent();
4426

45-
var fullText = agentResponseItem.Value.GetOutputText();
27+
var innerAgent = agent.GetService<ChatClientAgent>()!;
28+
var session = await innerAgent.CreateSessionAsync(conversationId);
29+
var response = await agent.RunAsync(message, session);
4630

47-
return Ok(new { data = fullText });
31+
return Ok(new { data = response.ToString() });
4832
}
4933

5034
[HttpPost]
5135
public async Task<IActionResult> Conversations()
5236
{
5337
// TODO [performance efficiency] Delay creating a conversation until the first user message arrives.
54-
ProjectConversationCreationOptions conversationOptions = new();
38+
FoundryAgent agent = agentResolver.GetAgent();
5539

56-
ProjectConversation conversation
57-
= await _projectClient.OpenAI.Conversations.CreateProjectConversationAsync(
58-
conversationOptions);
40+
var session = await agent.CreateConversationSessionAsync();
5941

60-
return Ok(new { id = conversation.Id });
42+
return Ok(new { id = session.ConversationId });
6143
}
6244
}

website/chatui/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
return projectClient;
1919
});
2020

21+
builder.Services.AddSingleton<FoundryAgentResolver>();
22+
2123
builder.Services.AddControllersWithViews();
2224

2325
builder.Services.AddCors(options =>

website/chatui/Views/Home/Index.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@
205205
});
206206
207207
async function sendPrompt(prompt) {
208-
const response = await fetch(`/chat/completions/${threadId}`, {
208+
const response = await fetch(`/chat/responses/${threadId}`, {
209209
method: "POST",
210210
headers: { "Content-Type": "application/json" },
211211
body: JSON.stringify(prompt)

website/chatui/appsettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
},
1414
"AllowedHosts": "*",
1515
"AIProjectEndpoint": "https://<foundryaccount-customsubdomain-name>.services.ai.azure.com/api/projects/<foundry-project-name>",
16-
"AIAgentId": ""
16+
"AIAgentId": "",
17+
"AIAgentVersion": ""
1718
}

website/chatui/chatui.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
<Folder Include="wwwroot\" />
1010
</ItemGroup>
1111
<ItemGroup>
12-
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
13-
<PackageReference Include="Azure.Identity" Version="1.19.0" />
12+
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0-*" />
1413
</ItemGroup>
1514
</Project>

0 commit comments

Comments
 (0)