RealtimeSession: persist user/assistant transcripts; emit assistant_done
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using backend.Conversations;
|
||||||
|
using backend.Data;
|
||||||
|
using backend.Realtime;
|
||||||
|
using backend.Tools;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace backend.tests;
|
||||||
|
|
||||||
|
public class RealtimeSessionTranscriptTests(ApiFactory factory) : IClassFixture<ApiFactory>
|
||||||
|
{
|
||||||
|
private readonly ApiFactory _factory = factory;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task User_and_assistant_transcripts_are_persisted_and_response_done_emits_assistant_done()
|
||||||
|
{
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
var log = scope.ServiceProvider.GetRequiredService<ConversationLog>();
|
||||||
|
var reg = new ToolRegistry(Array.Empty<ITool>());
|
||||||
|
var upstream = new ScriptedRealtimeUpstream();
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
var cfg = new RealtimeSettings("m", "v", "p", 30, new HashSet<string>());
|
||||||
|
var session = new RealtimeSession(Guid.NewGuid(), upstream, sink, log, reg, cfg,
|
||||||
|
new FakeDeviceChannel(), NullLogger.Instance);
|
||||||
|
|
||||||
|
upstream.Push(new JsonObject { ["type"] = "session.updated" });
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var run = session.RunAsync(cts.Token);
|
||||||
|
await sink.WaitForAsync("session_started", TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
upstream.Push(new JsonObject
|
||||||
|
{
|
||||||
|
["type"] = "conversation.item.input_audio_transcription.completed",
|
||||||
|
["transcript"] = "hello",
|
||||||
|
});
|
||||||
|
upstream.Push(new JsonObject
|
||||||
|
{
|
||||||
|
["type"] = "response.audio_transcript.done",
|
||||||
|
["transcript"] = "hi there",
|
||||||
|
});
|
||||||
|
upstream.Push(new JsonObject { ["type"] = "response.done" });
|
||||||
|
|
||||||
|
await sink.WaitForAsync("assistant_done", TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
var convoId = session.ConversationId!.Value;
|
||||||
|
var turns = await db.Turns
|
||||||
|
.Where(t => t.ConversationId == convoId)
|
||||||
|
.OrderBy(t => t.Index).ToListAsync();
|
||||||
|
turns.Select(t => t.Role).Should().Equal(TurnRole.User, TurnRole.Assistant);
|
||||||
|
turns[0].Text.Should().Be("hello");
|
||||||
|
turns[1].Text.Should().Be("hi there");
|
||||||
|
|
||||||
|
cts.Cancel();
|
||||||
|
await run;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,6 +121,24 @@ public class RealtimeSession
|
|||||||
case "response.audio.delta":
|
case "response.audio.delta":
|
||||||
await ForwardAudioDeltaAsync(evt, ct);
|
await ForwardAudioDeltaAsync(evt, ct);
|
||||||
break;
|
break;
|
||||||
|
case "conversation.item.input_audio_transcription.completed":
|
||||||
|
{
|
||||||
|
var text = (string?)evt["transcript"];
|
||||||
|
if (_conversation is not null && !string.IsNullOrEmpty(text))
|
||||||
|
await _log.AppendUserAsync(_conversation.Id, text!, ct);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "response.audio_transcript.done":
|
||||||
|
{
|
||||||
|
var text = (string?)evt["transcript"];
|
||||||
|
if (_conversation is not null && !string.IsNullOrEmpty(text))
|
||||||
|
await _log.AppendAssistantAsync(_conversation.Id, text!, ct);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "response.done":
|
||||||
|
await _output.WriteEnvelopeAsync(
|
||||||
|
new JsonObject { ["type"] = "assistant_done" }, ct);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user