PlotDirector/PlotLine/Data/CharacterEnrichmentRepository.cs

214 lines
9.4 KiB
C#

using System.Data;
using System.Text.Json;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface ICharacterEnrichmentRepository
{
Task<CharacterEnrichmentRun> QueueAsync(CharacterEnrichmentQueueRequest request);
Task<CharacterEnrichmentRun> QueueCharacterAsync(CharacterEnrichmentCharacterQueueRequest request);
Task<CharacterEnrichmentRun?> RetryAsync(int bookId, int userId);
Task<CharacterEnrichmentRun?> ClaimNextAsync(int leaseMinutes);
Task<CharacterEnrichmentRun?> GetCurrentAsync(int bookId);
Task<CharacterEnrichmentRun?> GetCurrentForCharacterAsync(int characterId, int userId);
Task<CharacterEnrichmentCharacterStatus?> GetCharacterStatusAsync(int characterId, int userId);
Task UpdateProgressAsync(CharacterEnrichmentProgressUpdate update);
Task CompleteAsync(CharacterEnrichmentCompletionRequest request, IReadOnlyList<CharacterEnrichmentCharacterResult> results);
Task FailAsync(int runId, string errorMessage, string? errorDetail, long? durationMs);
Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId);
Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId, int? characterId);
}
public sealed class CharacterEnrichmentRepository(ISqlConnectionFactory connectionFactory) : ICharacterEnrichmentRepository
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public async Task<CharacterEnrichmentRun> QueueAsync(CharacterEnrichmentQueueRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_Queue",
request,
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun> QueueCharacterAsync(CharacterEnrichmentCharacterQueueRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_QueueCharacter",
request,
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> RetryAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_Retry",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> ClaimNextAsync(int leaseMinutes)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_ClaimNext",
new { LeaseMinutes = leaseMinutes },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> GetCurrentAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_GetCurrentByBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> GetCurrentForCharacterAsync(int characterId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_GetCurrentByCharacter",
new { CharacterID = characterId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentCharacterStatus?> GetCharacterStatusAsync(int characterId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentCharacterStatus>(
"dbo.CharacterEnrichmentCharacterStatus_Get",
new { CharacterID = characterId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task UpdateProgressAsync(CharacterEnrichmentProgressUpdate update)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CharacterEnrichmentRun_UpdateProgress",
update,
commandType: CommandType.StoredProcedure);
}
public async Task CompleteAsync(CharacterEnrichmentCompletionRequest request, IReadOnlyList<CharacterEnrichmentCharacterResult> results)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CharacterEnrichmentRun_Complete",
new
{
request.CharacterEnrichmentRunID,
request.Model,
request.InputTokens,
request.OutputTokens,
request.TotalTokens,
request.DurationMs,
ResultsJson = JsonSerializer.Serialize(results, JsonOptions)
},
commandType: CommandType.StoredProcedure);
}
public async Task FailAsync(int runId, string errorMessage, string? errorDetail, long? durationMs)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CharacterEnrichmentRun_Fail",
new
{
CharacterEnrichmentRunID = runId,
ErrorMessage = errorMessage,
ErrorDetail = errorDetail,
DurationMs = durationMs
},
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId)
=> await BuildContextAsync(projectId, bookId, null);
public async Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId, int? characterId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.CharacterEnrichment_ContextGet",
new { ProjectID = projectId, BookID = bookId, CharacterID = characterId },
commandType: CommandType.StoredProcedure);
var book = await result.ReadSingleAsync<ContextBookRow>();
var characters = (await result.ReadAsync<CharacterEnrichmentCharacterContext>()).ToList();
var aliases = (await result.ReadAsync<ContextAliasRow>()).ToList();
var scenes = (await result.ReadAsync<ContextSceneRow>()).ToList();
var aliasesByCharacter = aliases
.GroupBy(alias => alias.CharacterID)
.ToDictionary(group => group.Key, group => (IReadOnlyList<string>)group.Select(alias => alias.Alias).ToList());
var scenesByCharacter = scenes
.GroupBy(scene => scene.CharacterID)
.ToDictionary(
group => group.Key,
group => (IReadOnlyList<CharacterEnrichmentSceneContext>)group.Select(scene => new CharacterEnrichmentSceneContext
{
SceneID = scene.SceneID,
ChapterID = scene.ChapterID,
ChapterNumber = scene.ChapterNumber,
ChapterTitle = scene.ChapterTitle,
SceneNumber = scene.SceneNumber,
SceneTitle = scene.SceneTitle,
SourceText = scene.SourceText
}).ToList());
return new CharacterEnrichmentContext
{
ProjectID = projectId,
BookID = bookId,
BookTitle = book.BookTitle,
StoryEra = book.StoryEra,
SeriesStartDate = book.SeriesStartDate,
Characters = characters.Select(character => new CharacterEnrichmentCharacterContext
{
CharacterID = character.CharacterID,
CharacterName = character.CharacterName,
BirthDate = character.BirthDate,
AgeAtSeriesStart = character.AgeAtSeriesStart,
Height = character.Height,
EyeColour = character.EyeColour,
DefaultDescription = character.DefaultDescription,
Aliases = aliasesByCharacter.GetValueOrDefault(character.CharacterID) ?? [],
Scenes = scenesByCharacter.GetValueOrDefault(character.CharacterID) ?? []
}).ToList()
};
}
private sealed class ContextBookRow
{
public string BookTitle { get; init; } = string.Empty;
public string? StoryEra { get; init; }
public DateTime? SeriesStartDate { get; init; }
}
private sealed class ContextAliasRow
{
public int CharacterID { get; init; }
public string Alias { get; init; } = string.Empty;
}
private sealed class ContextSceneRow
{
public int CharacterID { get; init; }
public int SceneID { get; init; }
public int ChapterID { get; init; }
public decimal ChapterNumber { get; init; }
public string ChapterTitle { get; init; } = string.Empty;
public decimal SceneNumber { get; init; }
public string SceneTitle { get; init; } = string.Empty;
public string SourceText { get; init; } = string.Empty;
}
}