188 lines
8.4 KiB
C#
188 lines
8.4 KiB
C#
using System.Text.Json;
|
|
using PlotLine.Data;
|
|
using PlotLine.Models;
|
|
using PlotLine.ViewModels;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IStoryIntelligenceResultPersistenceService
|
|
{
|
|
Task<int> SaveDisplayedRunAsync(int userId, string payload);
|
|
Task<StoryIntelligenceSavedRunsViewModel> ListRunsAsync();
|
|
Task<StoryIntelligenceSavedRunDetailViewModel?> GetRunDetailAsync(int runId);
|
|
}
|
|
|
|
public sealed class StoryIntelligenceResultPersistenceService(
|
|
IStoryIntelligenceResultRepository repository,
|
|
ILogger<StoryIntelligenceResultPersistenceService> logger) : IStoryIntelligenceResultPersistenceService
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
PropertyNameCaseInsensitive = true,
|
|
WriteIndented = true
|
|
};
|
|
|
|
public async Task<int> SaveDisplayedRunAsync(int userId, string payload)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(payload))
|
|
{
|
|
throw new InvalidOperationException("No dry-run result was submitted for saving.");
|
|
}
|
|
|
|
var dryRun = JsonSerializer.Deserialize<ChapterStoryIntelligenceDryRunViewModel>(payload, JsonOptions)
|
|
?? throw new InvalidOperationException("The submitted dry-run result could not be read.");
|
|
if (dryRun.Result is null)
|
|
{
|
|
throw new InvalidOperationException("The submitted dry-run result is empty.");
|
|
}
|
|
|
|
var request = BuildSaveRequest(userId, dryRun);
|
|
var runId = await repository.SaveAsync(request);
|
|
|
|
logger.LogInformation(
|
|
"Saved Story Intelligence admin dry-run. StoryIntelligenceRunID={StoryIntelligenceRunID} UserID={UserID} Status={Status} ChapterPromptVersion={ChapterPromptVersion} ScenePromptVersion={ScenePromptVersion} Model={Model} SceneCount={SceneCount} ValidationErrors={ValidationErrors} ValidationWarnings={ValidationWarnings} TotalDurationMs={TotalDurationMs} TotalTokens={TotalTokens}",
|
|
runId,
|
|
userId,
|
|
request.Status,
|
|
dryRun.Result.ChapterPromptVersion,
|
|
dryRun.Result.ScenePromptVersion,
|
|
dryRun.Result.Model,
|
|
request.SceneResults.Count,
|
|
CountErrors(dryRun.Result),
|
|
CountWarnings(dryRun.Result),
|
|
request.TotalDurationMs,
|
|
request.TotalTokens);
|
|
|
|
return runId;
|
|
}
|
|
|
|
public async Task<StoryIntelligenceSavedRunsViewModel> ListRunsAsync()
|
|
=> new() { Runs = await repository.ListRunsAsync() };
|
|
|
|
public async Task<StoryIntelligenceSavedRunDetailViewModel?> GetRunDetailAsync(int runId)
|
|
{
|
|
var run = await repository.GetRunAsync(runId);
|
|
if (run is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new StoryIntelligenceSavedRunDetailViewModel
|
|
{
|
|
Run = run,
|
|
ChapterResult = await repository.GetChapterResultAsync(runId),
|
|
SceneResults = await repository.ListSceneResultsAsync(runId)
|
|
};
|
|
}
|
|
|
|
private static StoryIntelligenceRunSaveRequest BuildSaveRequest(int userId, ChapterStoryIntelligenceDryRunViewModel dryRun)
|
|
{
|
|
var form = dryRun.Form;
|
|
var result = dryRun.Result!;
|
|
var completedUtc = DateTime.UtcNow;
|
|
var startedUtc = result.TotalDurationMs.HasValue
|
|
? completedUtc.AddMilliseconds(-result.TotalDurationMs.Value)
|
|
: completedUtc;
|
|
var errorCount = CountErrors(result);
|
|
var warningCount = CountWarnings(result);
|
|
var status = !string.IsNullOrWhiteSpace(result.ErrorMessage) || result.PipelineStopped
|
|
? StoryIntelligenceRunStatuses.Failed
|
|
: errorCount > 0 || warningCount > 0 || result.SceneBlocks.Any(block => !block.Success && block.SplitValid)
|
|
? StoryIntelligenceRunStatuses.CompletedWithIssues
|
|
: StoryIntelligenceRunStatuses.Completed;
|
|
|
|
return new StoryIntelligenceRunSaveRequest
|
|
{
|
|
UserID = userId,
|
|
ProjectID = form.ProjectID,
|
|
BookID = form.BookID,
|
|
Status = status,
|
|
SourceType = "AdminDryRun",
|
|
PromptVersion = $"Chapter={result.ChapterPromptVersion}; Scene={result.ScenePromptVersion}",
|
|
Model = result.Model,
|
|
StartedUtc = startedUtc,
|
|
CompletedUtc = completedUtc,
|
|
TotalInputTokens = result.TotalInputTokens,
|
|
TotalOutputTokens = result.TotalOutputTokens,
|
|
TotalTokens = SumTokens(result.TotalInputTokens, result.TotalOutputTokens),
|
|
TotalDurationMs = result.TotalDurationMs,
|
|
ErrorMessage = FirstError(result),
|
|
ChapterResult = BuildChapterResult(form, result),
|
|
SceneResults = result.SceneBlocks.Select(block => BuildSceneResult(form, result, block)).ToList()
|
|
};
|
|
}
|
|
|
|
private static StoryIntelligenceChapterResultSaveRequest BuildChapterResult(
|
|
ChapterStoryIntelligenceDryRunForm form,
|
|
ChapterStoryIntelligenceDryRunResultViewModel result)
|
|
=> new()
|
|
{
|
|
ProjectID = form.ProjectID,
|
|
BookID = form.BookID,
|
|
ChapterID = form.ChapterID,
|
|
ChapterNumber = form.ChapterNumber,
|
|
SourceLabel = CleanSourceLabel(form.SourceLabel),
|
|
PromptVersion = result.ChapterPromptVersion,
|
|
Model = result.Model,
|
|
RawResponseJson = result.ChapterRawResponseText,
|
|
OutputTextJson = result.ChapterJsonText,
|
|
ParsedJson = SerializeParsed(result.ParsedChapter),
|
|
ValidationErrorsCount = result.ChapterValidation?.Errors.Count ?? 0,
|
|
ValidationWarningsCount = result.ChapterValidation?.Warnings.Count ?? 0,
|
|
InputTokens = result.ChapterInputTokens,
|
|
OutputTokens = result.ChapterOutputTokens,
|
|
TotalTokens = SumTokens(result.ChapterInputTokens, result.ChapterOutputTokens),
|
|
DurationMs = result.ChapterDurationMs
|
|
};
|
|
|
|
private static StoryIntelligenceSceneResultSaveRequest BuildSceneResult(
|
|
ChapterStoryIntelligenceDryRunForm form,
|
|
ChapterStoryIntelligenceDryRunResultViewModel result,
|
|
ChapterStorySceneBlockViewModel block)
|
|
=> new()
|
|
{
|
|
ProjectID = form.ProjectID,
|
|
BookID = form.BookID,
|
|
ChapterID = form.ChapterID,
|
|
SceneID = null,
|
|
TemporarySceneNumber = block.TemporarySceneNumber,
|
|
StartParagraph = block.StartParagraph > 0 ? block.StartParagraph : null,
|
|
EndParagraph = block.EndParagraph > 0 ? block.EndParagraph : null,
|
|
SourceLabel = $"{CleanSourceLabel(form.SourceLabel)}, suggested scene {block.TemporarySceneNumber}",
|
|
PromptVersion = result.ScenePromptVersion,
|
|
Model = result.Model,
|
|
RawResponseJson = block.RawResponseText,
|
|
OutputTextJson = block.SceneJsonText,
|
|
ParsedJson = SerializeParsed(block.ParsedScene),
|
|
ValidationErrorsCount = block.Validation?.Errors.Count ?? 0,
|
|
ValidationWarningsCount = block.Validation?.Warnings.Count ?? 0,
|
|
InputTokens = block.InputTokens,
|
|
OutputTokens = block.OutputTokens,
|
|
TotalTokens = SumTokens(block.InputTokens, block.OutputTokens),
|
|
DurationMs = block.DurationMs
|
|
};
|
|
|
|
private static string SerializeParsed<T>(T? value)
|
|
=> value is null ? string.Empty : JsonSerializer.Serialize(value, JsonOptions);
|
|
|
|
private static string CleanSourceLabel(string? value)
|
|
=> string.IsNullOrWhiteSpace(value) ? "Admin pipeline test chapter" : value.Trim();
|
|
|
|
private static int? SumTokens(int? inputTokens, int? outputTokens)
|
|
=> inputTokens.HasValue || outputTokens.HasValue ? (inputTokens ?? 0) + (outputTokens ?? 0) : null;
|
|
|
|
private static int CountErrors(ChapterStoryIntelligenceDryRunResultViewModel result)
|
|
=> (result.ChapterValidation?.Errors.Count ?? 0)
|
|
+ result.SceneBlocks.Sum(block => block.Validation?.Errors.Count ?? 0);
|
|
|
|
private static int CountWarnings(ChapterStoryIntelligenceDryRunResultViewModel result)
|
|
=> (result.ChapterValidation?.Warnings.Count ?? 0)
|
|
+ result.SceneBlocks.Sum(block => block.Validation?.Warnings.Count ?? 0);
|
|
|
|
private static string? FirstError(ChapterStoryIntelligenceDryRunResultViewModel result)
|
|
=> !string.IsNullOrWhiteSpace(result.ErrorMessage)
|
|
? result.ErrorMessage
|
|
: result.SceneBlocks.FirstOrDefault(block => !string.IsNullOrWhiteSpace(block.ErrorMessage))?.ErrorMessage;
|
|
}
|