214 lines
9.4 KiB
C#
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;
|
|
}
|
|
}
|