|
2 | 2 |
|
3 | 3 | @using OpenAI |
4 | 4 | @using OpenAI.Chat |
| 5 | +@using Microsoft.Agents.AI |
| 6 | +@using Microsoft.Extensions.AI |
5 | 7 | @using Microsoft.Extensions.Configuration |
| 8 | +@using System.ComponentModel |
6 | 9 |
|
7 | 10 | @inject IConfiguration Configuration |
8 | 11 |
|
|
29 | 32 | <MudAvatar Color="Color.Primary" Size="Size.Small"> |
30 | 33 | @if (!string.IsNullOrEmpty(UserProfile?.ProfilePictureDataUrl ?? "")) |
31 | 34 | { |
32 | | - <MudImage Src="@UserProfile.ProfilePictureDataUrl" /> |
| 35 | + <MudImage Src="@(UserProfile?.ProfilePictureDataUrl ?? string.Empty)" /> |
33 | 36 | } |
34 | 37 | else |
35 | 38 | { |
|
49 | 52 | var bubbleClass = $"message-bubble {(message.Role == "user" ? "message-bubble-user" : "message-bubble-assistant")}"; |
50 | 53 | } |
51 | 54 | <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 | + } |
53 | 63 | </MudPaper> |
54 | | - @if (message.Role == "assistant") |
| 64 | + @if (message.Role == "assistant" && !string.IsNullOrEmpty(message.Content)) |
55 | 65 | { |
56 | 66 | <div class="message-actions"> |
57 | 67 | <MudIconButton Icon="@Icons.Material.Filled.ContentCopy" |
|
64 | 74 | </div> |
65 | 75 | </div> |
66 | 76 | } |
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 | | - } |
82 | 77 | <div @ref="messagesEndRef"></div> |
83 | 78 | </div> |
84 | 79 | </MudPaper> |
|
249 | 244 | </style> |
250 | 245 |
|
251 | 246 | @code { |
252 | | - // 定义本地数据模型 |
| 247 | + // Define local data model |
253 | 248 | private class ChatMessage |
254 | 249 | { |
255 | 250 | public string Role { get; set; } = "user"; |
|
259 | 254 | private List<ChatMessage> messages = new(); |
260 | 255 | private string userInput = string.Empty; |
261 | 256 | private bool isLoading = false; |
| 257 | + private bool preventDefaultEnter = false; |
262 | 258 | private ElementReference messagesEndRef; |
263 | | - private ChatClient? _chatClient; |
264 | | - private List<OpenAI.Chat.ChatMessage> _conversationHistory = new(); |
| 259 | + private AIAgent? _agent; |
| 260 | + private AgentThread? _thread; |
265 | 261 |
|
266 | 262 | [CascadingParameter] |
267 | 263 | private UserProfile? UserProfile { get; set; } |
|
274 | 270 |
|
275 | 271 | private async Task HandleKeyDown(KeyboardEventArgs e) |
276 | 272 | { |
277 | | - if (e.Key == "Enter" && e.CtrlKey) |
| 273 | + // Enter sends message, Shift+Enter for new line |
| 274 | + if (e.Key == "Enter" && !e.ShiftKey) |
278 | 275 | { |
| 276 | + preventDefaultEnter = true; |
279 | 277 | await SendAsync(); |
280 | 278 | } |
| 279 | + else |
| 280 | + { |
| 281 | + preventDefaultEnter = false; |
| 282 | + } |
281 | 283 | } |
282 | 284 |
|
283 | | - private void InitializeChatClient() |
| 285 | + private void InitializeAgent() |
284 | 286 | { |
285 | | - if (_chatClient is not null) return; |
| 287 | + if (_agent is not null) return; |
286 | 288 |
|
287 | 289 | var apiKey = Configuration["AISettings:OpenAIApiKey"]; |
288 | 290 | if (string.IsNullOrEmpty(apiKey) || apiKey == "your-openai-api-key") |
|
292 | 294 |
|
293 | 295 | var modelId = Configuration["AISettings:OpenAIModel"] ?? "gpt-5-nano"; |
294 | 296 | var client = new OpenAIClient(apiKey); |
295 | | - _chatClient = client.GetChatClient(modelId); |
| 297 | + var chatClient = client.GetChatClient(modelId); |
296 | 298 |
|
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 | + }); |
299 | 317 | } |
300 | 318 |
|
| 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 | + |
301 | 323 | private async Task SendAsync() |
302 | 324 | { |
303 | 325 | if (string.IsNullOrWhiteSpace(userInput) || isLoading) |
|
318 | 340 | await ScrollToBottomAsync(); |
319 | 341 |
|
320 | 342 | 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); |
321 | 351 | StateHasChanged(); |
322 | 352 |
|
323 | 353 | try |
324 | 354 | { |
325 | | - InitializeChatClient(); |
326 | | - |
327 | | - // 添加用户消息到对话历史 |
328 | | - _conversationHistory.Add(new UserChatMessage(userMessageContent)); |
| 355 | + InitializeAgent(); |
| 356 | + _thread ??= await _agent!.GetNewThreadAsync(); |
329 | 357 |
|
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)) |
335 | 360 | { |
336 | | - // 添加助手回复到对话历史 |
337 | | - _conversationHistory.Add(new AssistantChatMessage(assistantResponseText)); |
338 | | - |
339 | | - messages.Add(new ChatMessage |
| 361 | + if (!string.IsNullOrEmpty(update.Text)) |
340 | 362 | { |
341 | | - Role = "assistant", |
342 | | - Content = assistantResponseText |
343 | | - }); |
| 363 | + assistantMessage.Content += update.Text; |
| 364 | + StateHasChanged(); |
| 365 | + await ScrollToBottomAsync(); |
| 366 | + } |
344 | 367 | } |
345 | | - else |
| 368 | + |
| 369 | + if (string.IsNullOrWhiteSpace(assistantMessage.Content)) |
346 | 370 | { |
347 | | - throw new Exception("No response received from the API."); |
| 371 | + throw new Exception("No response received from the agent."); |
348 | 372 | } |
349 | 373 | } |
350 | 374 | catch (Exception ex) |
351 | 375 | { |
352 | 376 | var errorMessage = $"Error: {ex.Message}"; |
353 | | - messages.Add(new ChatMessage |
354 | | - { |
355 | | - Role = "assistant", |
356 | | - Content = errorMessage |
357 | | - }); |
| 377 | + assistantMessage.Content = errorMessage; |
| 378 | + StateHasChanged(); |
358 | 379 | Snackbar.Add(errorMessage, Severity.Error); |
359 | 380 | } |
360 | 381 | finally |
|
0 commit comments