-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChat.razor
More file actions
205 lines (171 loc) · 7.7 KB
/
Chat.razor
File metadata and controls
205 lines (171 loc) · 7.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
@page "/"
@using System.ComponentModel
@using System.Runtime.InteropServices
@using System.Text.RegularExpressions
@using System.Drawing
@using Microsoft.Extensions.AI
@using imageGeneratorSample.Web.Services.Images
@inject IChatClient ChatClient
@inject NavigationManager Nav
@inject SemanticSearch Search
@inject IImageCacheService ImageCache
@implements IDisposable
<PageTitle>Chat</PageTitle>
<ChatHeader OnNewChat="@ResetConversationAsync" />
<ChatMessageList Messages="@messages" InProgressMessage="@currentResponseMessage" >
<NoMessagesContent>
<div>To get started, try asking about these example documents. You can replace these with your own data and replace this message.</div>
<ChatCitation File="Example_Emergency_Survival_Kit.pdf"/>
<ChatCitation File="Example_GPS_Watch.pdf"/>
</NoMessagesContent>
</ChatMessageList>
<div class="chat-container">
<ChatSuggestions OnSelected="@AddUserMessageAsync" @ref="@chatSuggestions" />
<ChatInput OnSend="@AddUserMessageAsync" @ref="@chatInput" />
<SurveyPrompt /> @* Remove this line to eliminate the template survey message *@
</div>
@code {
private const string SystemPrompt = @"
You are an assistant who answers questions about information you retrieve.
Do not answer questions about anything else.
Use only simple markdown to format your responses.
Use the search tool to find relevant information. When you do this, end your
reply with citations in the special XML format:
<citation filename='string' page_number='number'>exact quote here</citation>
Always include the citation in your response if there are results.
The quote must be max 5 words, taken word-for-word from the search result, and is the basis for why the citation is relevant.
Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text.
";
private readonly ChatOptions chatOptions = new();
private readonly List<ChatMessage> messages = new();
private CancellationTokenSource? currentResponseCancellation;
private ChatMessage? currentResponseMessage;
private ChatInput? chatInput;
private ChatSuggestions? chatSuggestions;
protected override void OnInitialized()
{
messages.Add(new(ChatRole.System, SystemPrompt));
chatOptions.Tools =
[
AIFunctionFactory.Create(SearchAsync),
#pragma warning disable MEAI001 // Type is for evaluation purposes only
new HostedImageGenerationTool()
{
Options = new()
{
ModelId = "gpt-image-1",
StreamingCount = 3
}
}
];
}
private async Task AddUserMessageAsync(ChatMessage userMessage)
{
CancelAnyCurrentResponse();
// Add the user message to the conversation
messages.Add(userMessage);
chatSuggestions?.Clear();
await chatInput!.FocusAsync();
var chatMessages = chatOptions.ConversationId is null
? messages
: [userMessage];
// Stream and display a new response from the IChatClient
var responseText = new TextContent("");
currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
// initialize the new currentResponseMessage
await InvokeAsync(StateHasChanged);
currentResponseCancellation = new();
await foreach (var update in ChatClient.GetStreamingResponseAsync(chatMessages, chatOptions, currentResponseCancellation.Token))
{
chatOptions.ConversationId = update.ConversationId;
messages.AddMessages(update, filter: c => c is not TextContent);
await CacheImages(update.Contents, responseText);
responseText.Text += update.Text;
ChatMessageItem.NotifyChanged(currentResponseMessage);
}
// Store the final response in the conversation, and begin getting suggestions
messages.Add(currentResponseMessage!);
currentResponseMessage = null;
chatSuggestions?.Update(messages);
}
private async Task CacheImages(IList<AIContent> contents, TextContent textContent)
{
Dictionary<string, string> cachedUris = new();
for (int i = 0; i < contents.Count; i++)
{
var content = contents[i];
if (content is not ImageGenerationToolResultContent imageResult)
{
continue;
}
contents.RemoveAt(i--);
foreach (var dataContent in imageResult.Outputs?. OfType<DataContent>() ?? Array.Empty<DataContent>())
{
string? fileName = dataContent.Name;
fileName ??= imageResult.ImageId;
if (String.IsNullOrEmpty(fileName))
{
_ = dataContent.AdditionalProperties?.TryGetValue("ItemId", out fileName);
}
fileName ??= $"image_{Guid.NewGuid()}.png";
var bytes = MemoryMarshal.TryGetArray(dataContent.Data, out var segment)
? segment.Array!
: dataContent.Data.ToArray();
var cacheUri = await ImageCache.CacheImageAsync(bytes, dataContent.MediaType, fileName);
cachedUris[fileName] = Nav.ToAbsoluteUri(cacheUri).ToString();
}
}
// Find each image link in the text content and replace it with the cached URI
// Use a regex to find image links in the format 
// Replace each in order of appearance
textContent.Text = Regex.Replace(textContent.Text,
@"!\[([^\]]*)\]\(([^)]+)\)",
match =>
{
var fileName = match.Groups[1].Value;
if (!cachedUris.TryGetValue(fileName, out var uri))
{
return match.Value; // no cached URI found, return original
}
cachedUris.Remove(fileName);
return $"";
});
// if we have any images remaining, just add them to the end
while (cachedUris.Count > 0)
{
var imageUri = cachedUris.First();
cachedUris.Remove(imageUri.Key);
textContent.Text += $"\n";
}
}
private void CancelAnyCurrentResponse()
{
// If a response was cancelled while streaming, include it in the conversation so it's not lost
if (currentResponseMessage is not null)
{
messages.Add(currentResponseMessage);
}
currentResponseCancellation?.Cancel();
currentResponseMessage = null;
}
private async Task ResetConversationAsync()
{
CancelAnyCurrentResponse();
messages.Clear();
messages.Add(new(ChatRole.System, SystemPrompt));
chatSuggestions?.Clear();
await chatInput!.FocusAsync();
}
[Description("Searches for information using a phrase or keyword")]
private async Task<IEnumerable<string>> SearchAsync(
[Description("The phrase to search for.")] string searchPhrase,
[Description("If possible, specify the filename to search that file only. If not provided or empty, the search includes all files.")] string? filenameFilter = null)
{
await InvokeAsync(StateHasChanged);
var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5);
return results.Select(result =>
$"<result filename=\"{result.DocumentId}\" page_number=\"{result.PageNumber}\">{result.Text}</result>");
}
public void Dispose()
=> currentResponseCancellation?.Cancel();
}