Skip to content

Commit bddaa4d

Browse files
committed
Refactor chatbot to use Microsoft.Agents.AI agent API
Replaces OpenAI.Chat.ChatClient with agent-based API for chat. Implements streaming assistant responses and improves UI feedback. Moves loading spinner into assistant bubble; removes old loading row. Message copy button now only shows for non-empty assistant replies. Improves avatar rendering and keyboard handling (Enter/Shift+Enter). Displays errors directly in assistant bubble for better UX.
1 parent c06166b commit bddaa4d

File tree

1 file changed

+71
-50
lines changed

1 file changed

+71
-50
lines changed

src/Server.UI/Pages/AI/Chatbot.razor

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
@using OpenAI
44
@using OpenAI.Chat
5+
@using Microsoft.Agents.AI
6+
@using Microsoft.Extensions.AI
57
@using Microsoft.Extensions.Configuration
8+
@using System.ComponentModel
69

710
@inject IConfiguration Configuration
811

@@ -29,7 +32,7 @@
2932
<MudAvatar Color="Color.Primary" Size="Size.Small">
3033
@if (!string.IsNullOrEmpty(UserProfile?.ProfilePictureDataUrl ?? ""))
3134
{
32-
<MudImage Src="@UserProfile.ProfilePictureDataUrl" />
35+
<MudImage Src="@(UserProfile?.ProfilePictureDataUrl ?? string.Empty)" />
3336
}
3437
else
3538
{
@@ -49,9 +52,16 @@
4952
var bubbleClass = $"message-bubble {(message.Role == "user" ? "message-bubble-user" : "message-bubble-assistant")}";
5053
}
5154
<MudPaper Class="@bubbleClass" Elevation="1">
52-
<MudText Typo="Typo.body1" Class="message-text">@message.Content</MudText>
55+
@if (message.Role == "assistant" && string.IsNullOrEmpty(message.Content) && isLoading)
56+
{
57+
<MudProgressCircular Color="Color.Secondary" Size="Size.Small" Indeterminate="true" />
58+
}
59+
else
60+
{
61+
<MudText Typo="Typo.body1" Class="message-text">@message.Content</MudText>
62+
}
5363
</MudPaper>
54-
@if (message.Role == "assistant")
64+
@if (message.Role == "assistant" && !string.IsNullOrEmpty(message.Content))
5565
{
5666
<div class="message-actions">
5767
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
@@ -64,21 +74,6 @@
6474
</div>
6575
</div>
6676
}
67-
@if (isLoading)
68-
{
69-
<div class="message-row message-assistant">
70-
<div class="message-avatar">
71-
<MudAvatar Color="Color.Secondary" Size="Size.Small">
72-
<MudIcon Icon="@Icons.Material.Filled.SmartToy" />
73-
</MudAvatar>
74-
</div>
75-
<div class="message-content-wrapper">
76-
<MudPaper Class="message-bubble message-bubble-assistant" Elevation="1">
77-
<MudProgressCircular Color="Color.Secondary" Size="Size.Small" Indeterminate="true" />
78-
</MudPaper>
79-
</div>
80-
</div>
81-
}
8277
<div @ref="messagesEndRef"></div>
8378
</div>
8479
</MudPaper>
@@ -249,7 +244,7 @@
249244
</style>
250245

251246
@code {
252-
// 定义本地数据模型
247+
// Define local data model
253248
private class ChatMessage
254249
{
255250
public string Role { get; set; } = "user";
@@ -259,9 +254,10 @@
259254
private List<ChatMessage> messages = new();
260255
private string userInput = string.Empty;
261256
private bool isLoading = false;
257+
private bool preventDefaultEnter = false;
262258
private ElementReference messagesEndRef;
263-
private ChatClient? _chatClient;
264-
private List<OpenAI.Chat.ChatMessage> _conversationHistory = new();
259+
private AIAgent? _agent;
260+
private AgentThread? _thread;
265261

266262
[CascadingParameter]
267263
private UserProfile? UserProfile { get; set; }
@@ -274,15 +270,21 @@
274270

275271
private async Task HandleKeyDown(KeyboardEventArgs e)
276272
{
277-
if (e.Key == "Enter" && e.CtrlKey)
273+
// Enter sends message, Shift+Enter for new line
274+
if (e.Key == "Enter" && !e.ShiftKey)
278275
{
276+
preventDefaultEnter = true;
279277
await SendAsync();
280278
}
279+
else
280+
{
281+
preventDefaultEnter = false;
282+
}
281283
}
282284

283-
private void InitializeChatClient()
285+
private void InitializeAgent()
284286
{
285-
if (_chatClient is not null) return;
287+
if (_agent is not null) return;
286288

287289
var apiKey = Configuration["AISettings:OpenAIApiKey"];
288290
if (string.IsNullOrEmpty(apiKey) || apiKey == "your-openai-api-key")
@@ -292,12 +294,32 @@
292294

293295
var modelId = Configuration["AISettings:OpenAIModel"] ?? "gpt-5-nano";
294296
var client = new OpenAIClient(apiKey);
295-
_chatClient = client.GetChatClient(modelId);
297+
var chatClient = client.GetChatClient(modelId);
296298

297-
// 添加系统提示
298-
_conversationHistory.Add(new SystemChatMessage("You are a helpful AI assistant. Be concise and accurate in your responses."));
299+
const string instructions = "You are a helpful AI assistant. Be concise and accurate in your responses.";
300+
_agent = chatClient.AsAIAgent(new ChatClientAgentOptions
301+
{
302+
Name = "Chatbot",
303+
ChatOptions = new()
304+
{
305+
Instructions = instructions,
306+
Tools = [AIFunctionFactory.Create(GetCurrentDate)]
307+
},
308+
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(
309+
new InMemoryChatMessageStore(
310+
#pragma warning disable MEAI001
311+
new MessageCountingChatReducer(5),
312+
#pragma warning restore MEAI001
313+
ctx.SerializedState,
314+
ctx.JsonSerializerOptions,
315+
InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded))
316+
});
299317
}
300318

319+
[Description("Get the current date (local server time) in ISO-8601 format (yyyy-MM-dd).")]
320+
private static string GetCurrentDate()
321+
=> DateTimeOffset.Now.ToString("yyyy-MM-dd");
322+
301323
private async Task SendAsync()
302324
{
303325
if (string.IsNullOrWhiteSpace(userInput) || isLoading)
@@ -318,43 +340,42 @@
318340
await ScrollToBottomAsync();
319341

320342
isLoading = true;
343+
344+
// Add an empty assistant message for streaming updates
345+
var assistantMessage = new ChatMessage
346+
{
347+
Role = "assistant",
348+
Content = string.Empty
349+
};
350+
messages.Add(assistantMessage);
321351
StateHasChanged();
322352

323353
try
324354
{
325-
InitializeChatClient();
326-
327-
// 添加用户消息到对话历史
328-
_conversationHistory.Add(new UserChatMessage(userMessageContent));
355+
InitializeAgent();
356+
_thread ??= await _agent!.GetNewThreadAsync();
329357

330-
// 调用 OpenAI API
331-
var response = await _chatClient!.CompleteChatAsync(_conversationHistory);
332-
var assistantResponseText = response.Value.Content[0].Text;
333-
334-
if (!string.IsNullOrEmpty(assistantResponseText))
358+
var options = new AgentRunOptions();
359+
await foreach (var update in _agent!.RunStreamingAsync(userMessageContent, _thread, options, CancellationToken.None))
335360
{
336-
// 添加助手回复到对话历史
337-
_conversationHistory.Add(new AssistantChatMessage(assistantResponseText));
338-
339-
messages.Add(new ChatMessage
361+
if (!string.IsNullOrEmpty(update.Text))
340362
{
341-
Role = "assistant",
342-
Content = assistantResponseText
343-
});
363+
assistantMessage.Content += update.Text;
364+
StateHasChanged();
365+
await ScrollToBottomAsync();
366+
}
344367
}
345-
else
368+
369+
if (string.IsNullOrWhiteSpace(assistantMessage.Content))
346370
{
347-
throw new Exception("No response received from the API.");
371+
throw new Exception("No response received from the agent.");
348372
}
349373
}
350374
catch (Exception ex)
351375
{
352376
var errorMessage = $"Error: {ex.Message}";
353-
messages.Add(new ChatMessage
354-
{
355-
Role = "assistant",
356-
Content = errorMessage
357-
});
377+
assistantMessage.Content = errorMessage;
378+
StateHasChanged();
358379
Snackbar.Add(errorMessage, Severity.Error);
359380
}
360381
finally

0 commit comments

Comments
 (0)