diff --git a/PlotLine/Data/StoryIntelligenceResultRepository.cs b/PlotLine/Data/StoryIntelligenceResultRepository.cs index c540a86..9c0c89d 100644 --- a/PlotLine/Data/StoryIntelligenceResultRepository.cs +++ b/PlotLine/Data/StoryIntelligenceResultRepository.cs @@ -41,6 +41,9 @@ public interface IStoryIntelligenceResultRepository public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceResultRepository { + private static readonly SemaphoreSlim QueueSupportLock = new(1, 1); + private static bool queueSupportEnsured; + public async Task SaveAsync(StoryIntelligenceRunSaveRequest request) { using var connection = connectionFactory.CreateConnection(); @@ -160,6 +163,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn public async Task QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request) { using var connection = connectionFactory.CreateConnection(); + await EnsureKnownCharacterQueueSupportAsync(connection); return await connection.QuerySingleAsync( "dbo.StoryIntelligenceRun_QueueAdminText", new @@ -185,6 +189,129 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn commandType: CommandType.StoredProcedure); } + private static async Task EnsureKnownCharacterQueueSupportAsync(IDbConnection connection) + { + if (queueSupportEnsured) + { + return; + } + + await QueueSupportLock.WaitAsync(); + try + { + if (queueSupportEnsured) + { + return; + } + + await connection.ExecuteAsync( + """ + IF COL_LENGTH(N'dbo.StoryIntelligenceRuns', N'KnownCharactersJson') IS NULL + BEGIN + ALTER TABLE dbo.StoryIntelligenceRuns ADD KnownCharactersJson nvarchar(max) NULL; + END; + """); + + await connection.ExecuteAsync( + """ + CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_QueueAdminText + @UserID int, + @ProjectID int = NULL, + @BookID int = NULL, + @ChapterID int = NULL, + @ChapterNumber decimal(9, 2) = NULL, + @SourceType nvarchar(50), + @SourceLabel nvarchar(300), + @SourceText nvarchar(max), + @SourceFileName nvarchar(260) = NULL, + @SourceFileSizeBytes bigint = NULL, + @SourceWordCount int = NULL, + @SourceCharacterCount int = NULL, + @SourceParagraphCount int = NULL, + @SourceChapterCount int = NULL, + @Model nvarchar(100), + @PromptVersionsSummary nvarchar(500), + @KnownCharactersJson nvarchar(max) = NULL + AS + BEGIN + SET NOCOUNT ON; + + INSERT dbo.StoryIntelligenceRuns + ( + UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel, SourceText, + SourceFileName, SourceFileSizeBytes, SourceWordCount, SourceCharacterCount, SourceParagraphCount, + SourceChapterCount, Model, PromptVersion, PromptVersionsSummary, KnownCharactersJson, StartedUtc, + CurrentStage, CurrentMessage, CompletedScenes, FailedScenes + ) + VALUES + ( + @UserID, @ProjectID, @BookID, @ChapterID, @ChapterNumber, N'Pending', @SourceType, @SourceLabel, @SourceText, + @SourceFileName, @SourceFileSizeBytes, @SourceWordCount, @SourceCharacterCount, @SourceParagraphCount, + @SourceChapterCount, @Model, @PromptVersionsSummary, @PromptVersionsSummary, @KnownCharactersJson, + SYSUTCDATETIME(), N'Pending', N'Queued for Story Intelligence processing.', 0, 0 + ); + + SELECT CAST(SCOPE_IDENTITY() AS int) AS StoryIntelligenceRunID; + END; + """); + + await connection.ExecuteAsync( + """ + CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ClaimNextPending + AS + BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + DECLARE @StoryIntelligenceRunID int; + + SELECT TOP (1) @StoryIntelligenceRunID = StoryIntelligenceRunID + FROM dbo.StoryIntelligenceRuns WITH (UPDLOCK, READPAST) + WHERE Status = N'Pending' + ORDER BY CreatedUtc; + + IF @StoryIntelligenceRunID IS NULL + BEGIN + SELECT TOP (0) + StoryIntelligenceRunID, UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel, + SourceText, SourceWordCount, SourceCharacterCount, SourceParagraphCount, SourceChapterCount, PromptVersion, + PromptVersionsSummary, KnownCharactersJson, Model, StartedUtc, CompletedUtc, FailureStage, TotalInputTokens, + TotalOutputTokens, TotalTokens, TotalDurationMs, EstimatedCostGBP, EstimatedCostUSD, + ErrorMessage, ErrorDetail, CurrentStage, CurrentMessage, TotalDetectedScenes, + CompletedScenes, FailedScenes, CancellationRequestedUtc, CancelledUtc, CreatedUtc, UpdatedUtc + FROM dbo.StoryIntelligenceRuns; + RETURN; + END; + + UPDATE dbo.StoryIntelligenceRuns + SET Status = N'Running', + StartedUtc = SYSUTCDATETIME(), + CurrentStage = N'ChapterStructure', + CurrentMessage = N'Running Chapter Structure analysis.', + UpdatedUtc = SYSUTCDATETIME() + WHERE StoryIntelligenceRunID = @StoryIntelligenceRunID + AND Status = N'Pending'; + + SELECT + StoryIntelligenceRunID, UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel, + SourceText, SourceWordCount, SourceCharacterCount, SourceParagraphCount, SourceChapterCount, PromptVersion, + PromptVersionsSummary, KnownCharactersJson, Model, StartedUtc, CompletedUtc, FailureStage, TotalInputTokens, + TotalOutputTokens, TotalTokens, TotalDurationMs, EstimatedCostGBP, EstimatedCostUSD, + ErrorMessage, ErrorDetail, CurrentStage, CurrentMessage, TotalDetectedScenes, + CompletedScenes, FailedScenes, CancellationRequestedUtc, CancelledUtc, CreatedUtc, UpdatedUtc + FROM dbo.StoryIntelligenceRuns + WHERE StoryIntelligenceRunID = @StoryIntelligenceRunID; + END; + """); + + queueSupportEnsured = true; + } + finally + { + QueueSupportLock.Release(); + } + } + public async Task ClaimNextPendingAsync() { using var connection = connectionFactory.CreateConnection();