I have a Blazor Server application. With this application, I am "talking" with MistralAI. This REST API endpoint has a streaming token you can set that will allow it to "stream" responses to you.
In my application, I have a service that contacts MistralAI for this conversation. I have implemented the Observer pattern so that I can update my .RAZOR page whenever I get a response from MistralAI:
MyService.cs:
public class MyService {
private List<Reader> readers = new List<Reader>();
private AIResponse response;
public async Task<AIResponse> myTask(){
Reader newReader = new MyPage();
readers.Add(newReader);
// Send requests to and receive responses from MistralAI
addWord(receivedJSONString);
notifyReaders();
}
public void notifyReaders()
{
foreach (var reader in readers) {
reader.updateWords(response);
}
}
public void addWord(AIResponse newResponse)
{
response = newResponse;
}
}
public interface Reader {
void updateResponses(AIResponse newResponse);
}
MyPage.razor
@implements Reader
<div>
@foreach(var newResponse in responses){
@newResponse
}
</div>
@code {
private List<AIResponse> responses { get; set; } = new List<AIResponse>();
private async Task QueryAI(){
AIResponse streamingAIResponse = await myService.myTask(someString);
}
public void updateResponses(AIResponse newResponse){
responses.Add(newResponse);
Console.Write(newResponse);
StateHasChanged();
}
}
I can successfully get the responses to my .RAZOR page, I can see them in the console when I Console.Write
, but I cannot update the UI with the new content; I keep getting the error The render handle is not yet assigned
.
So, how can I use the StateHasChanged()
from within my implemented interface?
Or, how can I update my UI with the new content?
UPDATES:
@code {
[Inject]
public IMyService myService { get; set; }
private String queryString { get; set; } = null;
private List<AIResponse> responses { get; set; } = new List<AIResponse>();
private Conversation conversation { get; set; }
private ElementReference aiQuery;
protected override void OnInitialized()
{
if (conversation == null)
{
conversation = new onversation();
conversation.messages = new List<Message>();
}
}
private async Task QueryAI()
{
Message message = new Message("user", queryString);
conversation.messages.Add(message);
StreamingQueryResponse streamingAIResponse = await myService.getStreamingAIResponseAsync(conversation);
}
public void updateWords(StreamingQueryResponse newResponse)
{
responses.Add(newResponse);
Console.Write(newResponse);
}
}
The observer pattern generally has a service with an event you subscribe to in your consuming page.
The following isn't working code, but should have all the elements to understand the service / page relationship:
-the service does all the API interactions. It sets up the async call, and every time new message info comes in, it fires the event
-the page or component subscribes to the service event by adding an event handler to process each message, and (importantly) removes it in Dispose
.
MistralService.cs
namespace MyApp.Code
{
public class MistralService
{
public event Action<string> OnMessageReceived;
public async Task StartStreamingAsync(string apiKey)
{
using var httpClient = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "mistral.api/endpoint");
request.Headers.Add("Authorization", $"Bearer {apiKey}");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
OnMessageReceived?.Invoke(line);
}
}
}
}
ConsumingPage.razor
@using MyApp.Code
@inject MistralService MS
<h3>ConsumingPage</h3>
<button @onclick=StartStreaming>Start Streaming</button>
<div>@streamedText</div>
@code {
string streamedText = "";
protected override void OnInitialized()
{
MS.OnMessageReceived += HandleMessage;
}
async Task StartStreaming()
{
await MS.StartStreamingAsync("theAPIkey");
}
async void HandleMessage(string Message)
{
streamedText += Message + "<br/>";
await InvokeAsync(StateHasChanged);
}
public void Dispose()
{
MS.OnMessageReceived -= HandleMessage;
}
}
Remember to add MistralService to your service in Program.cs
:
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddScoped<MistralService>();