PlotDirector/PlotLine/Data/StoryIntelligenceResultRepository.cs

375 lines
15 KiB
C#

using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IStoryIntelligenceResultRepository
{
Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request);
Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request);
Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync();
Task UpdateProgressAsync(
int runId,
string? status = null,
string? currentStage = null,
string? currentMessage = null,
int? totalDetectedScenes = null,
int? completedScenes = null,
int? failedScenes = null,
int? totalInputTokens = null,
int? totalOutputTokens = null,
int? totalTokens = null,
long? totalDurationMs = null,
decimal? estimatedCostGBP = null,
decimal? estimatedCostUSD = null);
Task CompleteRunAsync(int runId, string status, int? totalInputTokens, int? totalOutputTokens, int? totalTokens, long? totalDurationMs, decimal? estimatedCostGBP, decimal? estimatedCostUSD);
Task FailRunAsync(int runId, string failureStage, string errorMessage, string? errorDetail, long? totalDurationMs);
Task RequestCancelAsync(int runId);
Task CancelRunAsync(int runId, long? totalDurationMs);
Task<int> SaveChapterResultAsync(int runId, StoryIntelligenceChapterResultSaveRequest chapter);
Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene);
Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync();
Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId);
Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId);
Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId);
}
public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceResultRepository
{
public async Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request)
{
using var connection = connectionFactory.CreateConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
var runId = await connection.QuerySingleAsync<int>(
"dbo.StoryIntelligenceRun_Save",
new
{
request.UserID,
request.ProjectID,
request.BookID,
request.Status,
request.SourceType,
request.SourceFileName,
request.SourceFileSizeBytes,
request.SourceWordCount,
request.SourceCharacterCount,
request.SourceChapterCount,
request.SourceDetectedImagesCount,
request.SourceDetectedTablesCount,
request.SourceDetectedFootnotesCount,
request.SourceDetectedCommentsCount,
request.PromptVersion,
request.Model,
request.StartedUtc,
request.CompletedUtc,
request.FailureStage,
request.TotalInputTokens,
request.TotalOutputTokens,
request.TotalTokens,
request.TotalDurationMs,
request.EstimatedCostGBP,
request.EstimatedCostUSD,
request.PromptVersionsSummary,
request.ErrorMessage,
request.ErrorDetail
},
transaction,
commandType: CommandType.StoredProcedure);
int? chapterResultId = null;
if (request.ChapterResult is not null)
{
var chapter = request.ChapterResult;
chapterResultId = await connection.QuerySingleAsync<int>(
"dbo.StoryIntelligenceChapterResult_Save",
new
{
StoryIntelligenceRunID = runId,
chapter.ProjectID,
chapter.BookID,
chapter.ChapterID,
chapter.ChapterNumber,
chapter.SourceLabel,
chapter.PromptVersion,
chapter.Model,
chapter.RawResponseJson,
chapter.OutputTextJson,
chapter.ParsedJson,
chapter.ValidationErrorsCount,
chapter.ValidationWarningsCount,
chapter.InputTokens,
chapter.OutputTokens,
chapter.TotalTokens,
chapter.DurationMs
},
transaction,
commandType: CommandType.StoredProcedure);
}
foreach (var scene in request.SceneResults)
{
await connection.ExecuteAsync(
"dbo.StoryIntelligenceSceneResult_Save",
new
{
StoryIntelligenceRunID = runId,
ChapterResultID = chapterResultId,
scene.ProjectID,
scene.BookID,
scene.ChapterID,
scene.SceneID,
scene.TemporarySceneNumber,
scene.StartParagraph,
scene.EndParagraph,
scene.SourceLabel,
scene.PromptVersion,
scene.Model,
scene.RawResponseJson,
scene.OutputTextJson,
scene.ParsedJson,
scene.ValidationErrorsCount,
scene.ValidationWarningsCount,
scene.InputTokens,
scene.OutputTokens,
scene.TotalTokens,
scene.DurationMs
},
transaction,
commandType: CommandType.StoredProcedure);
}
transaction.Commit();
return runId;
}
catch
{
transaction.Rollback();
throw;
}
}
public async Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.StoryIntelligenceRun_QueueAdminText",
new
{
request.UserID,
request.ProjectID,
request.BookID,
request.ChapterID,
request.ChapterNumber,
request.SourceType,
request.SourceLabel,
SourceText = request.ChapterText,
request.SourceWordCount,
request.SourceCharacterCount,
request.SourceChapterCount,
request.Model,
request.PromptVersionsSummary
},
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync()
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceQueuedRun>(
"dbo.StoryIntelligenceRun_ClaimNextPending",
commandType: CommandType.StoredProcedure);
}
public async Task UpdateProgressAsync(
int runId,
string? status = null,
string? currentStage = null,
string? currentMessage = null,
int? totalDetectedScenes = null,
int? completedScenes = null,
int? failedScenes = null,
int? totalInputTokens = null,
int? totalOutputTokens = null,
int? totalTokens = null,
long? totalDurationMs = null,
decimal? estimatedCostGBP = null,
decimal? estimatedCostUSD = null)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceRun_UpdateProgress",
new
{
StoryIntelligenceRunID = runId,
Status = status,
CurrentStage = currentStage,
CurrentMessage = currentMessage,
TotalDetectedScenes = totalDetectedScenes,
CompletedScenes = completedScenes,
FailedScenes = failedScenes,
TotalInputTokens = totalInputTokens,
TotalOutputTokens = totalOutputTokens,
TotalTokens = totalTokens,
TotalDurationMs = totalDurationMs,
EstimatedCostGBP = estimatedCostGBP,
EstimatedCostUSD = estimatedCostUSD
},
commandType: CommandType.StoredProcedure);
}
public async Task CompleteRunAsync(int runId, string status, int? totalInputTokens, int? totalOutputTokens, int? totalTokens, long? totalDurationMs, decimal? estimatedCostGBP, decimal? estimatedCostUSD)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceRun_Complete",
new
{
StoryIntelligenceRunID = runId,
Status = status,
TotalInputTokens = totalInputTokens,
TotalOutputTokens = totalOutputTokens,
TotalTokens = totalTokens,
TotalDurationMs = totalDurationMs,
EstimatedCostGBP = estimatedCostGBP,
EstimatedCostUSD = estimatedCostUSD
},
commandType: CommandType.StoredProcedure);
}
public async Task FailRunAsync(int runId, string failureStage, string errorMessage, string? errorDetail, long? totalDurationMs)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceRun_Fail",
new
{
StoryIntelligenceRunID = runId,
FailureStage = failureStage,
ErrorMessage = errorMessage,
ErrorDetail = errorDetail,
TotalDurationMs = totalDurationMs
},
commandType: CommandType.StoredProcedure);
}
public async Task RequestCancelAsync(int runId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceRun_RequestCancel",
new { StoryIntelligenceRunID = runId },
commandType: CommandType.StoredProcedure);
}
public async Task CancelRunAsync(int runId, long? totalDurationMs)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceRun_Cancel",
new { StoryIntelligenceRunID = runId, TotalDurationMs = totalDurationMs },
commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveChapterResultAsync(int runId, StoryIntelligenceChapterResultSaveRequest chapter)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.StoryIntelligenceChapterResult_Save",
new
{
StoryIntelligenceRunID = runId,
chapter.ProjectID,
chapter.BookID,
chapter.ChapterID,
chapter.ChapterNumber,
chapter.SourceLabel,
chapter.PromptVersion,
chapter.Model,
chapter.RawResponseJson,
chapter.OutputTextJson,
chapter.ParsedJson,
chapter.ValidationErrorsCount,
chapter.ValidationWarningsCount,
chapter.InputTokens,
chapter.OutputTokens,
chapter.TotalTokens,
chapter.DurationMs
},
commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.StoryIntelligenceSceneResult_Save",
new
{
StoryIntelligenceRunID = runId,
ChapterResultID = chapterResultId,
scene.ProjectID,
scene.BookID,
scene.ChapterID,
scene.SceneID,
scene.TemporarySceneNumber,
scene.StartParagraph,
scene.EndParagraph,
scene.SourceLabel,
scene.PromptVersion,
scene.Model,
scene.RawResponseJson,
scene.OutputTextJson,
scene.ParsedJson,
scene.ValidationErrorsCount,
scene.ValidationWarningsCount,
scene.InputTokens,
scene.OutputTokens,
scene.TotalTokens,
scene.DurationMs
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryIntelligenceSavedRunListItem>(
"dbo.StoryIntelligenceRun_ListAdmin",
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSavedRun>(
"dbo.StoryIntelligenceRun_GetAdmin",
new { StoryIntelligenceRunID = runId },
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSavedChapterResult>(
"dbo.StoryIntelligenceChapterResult_GetByRun",
new { StoryIntelligenceRunID = runId },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryIntelligenceSavedSceneResult>(
"dbo.StoryIntelligenceSceneResult_ListByRun",
new { StoryIntelligenceRunID = runId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
}