PlotDirector/PlotLine/Sql/183_Phase27J_CharacterEnrichmentRuns.sql

466 lines
19 KiB
Transact-SQL

SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.CharacterEnrichmentRuns', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterEnrichmentRuns
(
CharacterEnrichmentRunID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterEnrichmentRuns PRIMARY KEY,
ProjectID int NOT NULL,
BookID int NOT NULL,
UserID int NOT NULL,
StoryIntelligenceBookPipelineID int NULL,
CharacterResolutionVersion nvarchar(120) NOT NULL,
Status nvarchar(30) NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_Status DEFAULT N'Queued',
TotalCharacters int NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_TotalCharacters DEFAULT 0,
ProcessedCharacters int NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_ProcessedCharacters DEFAULT 0,
CurrentStage nvarchar(120) NULL,
CurrentMessage nvarchar(300) NULL,
QueuedUtc datetime2 NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_QueuedUtc DEFAULT SYSUTCDATETIME(),
StartedUtc datetime2 NULL,
CompletedUtc datetime2 NULL,
FailedUtc datetime2 NULL,
AttemptCount int NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_AttemptCount DEFAULT 0,
Model nvarchar(100) NULL,
InputTokens int NULL,
OutputTokens int NULL,
TotalTokens int NULL,
DurationMs bigint NULL,
ErrorMessage nvarchar(max) NULL,
ErrorDetail nvarchar(max) NULL,
IsCurrent bit NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_IsCurrent DEFAULT 1,
UpdatedUtc datetime2 NOT NULL CONSTRAINT DF_CharacterEnrichmentRuns_UpdatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_CharacterEnrichmentRuns_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_CharacterEnrichmentRuns_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID) ON DELETE CASCADE,
CONSTRAINT FK_CharacterEnrichmentRuns_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID),
CONSTRAINT FK_CharacterEnrichmentRuns_Pipeline FOREIGN KEY (StoryIntelligenceBookPipelineID) REFERENCES dbo.StoryIntelligenceBookPipelines(StoryIntelligenceBookPipelineID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_CharacterEnrichmentRuns_Queue' AND object_id = OBJECT_ID(N'dbo.CharacterEnrichmentRuns'))
CREATE INDEX IX_CharacterEnrichmentRuns_Queue ON dbo.CharacterEnrichmentRuns(Status, QueuedUtc) INCLUDE (BookID, UserID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_CharacterEnrichmentRuns_CurrentVersion' AND object_id = OBJECT_ID(N'dbo.CharacterEnrichmentRuns'))
CREATE UNIQUE INDEX UX_CharacterEnrichmentRuns_CurrentVersion
ON dbo.CharacterEnrichmentRuns(BookID, CharacterResolutionVersion)
WHERE IsCurrent = 1 AND Status IN (N'Queued', N'Running', N'Completed');
GO
IF OBJECT_ID(N'dbo.CharacterEnrichmentEvidence', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterEnrichmentEvidence
(
CharacterEnrichmentEvidenceID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterEnrichmentEvidence PRIMARY KEY,
CharacterEnrichmentRunID int NOT NULL,
CharacterID int NOT NULL,
SceneID int NULL,
FieldName nvarchar(80) NOT NULL,
Value nvarchar(500) NULL,
Evidence nvarchar(700) NULL,
Confidence decimal(5,4) NOT NULL CONSTRAINT DF_CharacterEnrichmentEvidence_Confidence DEFAULT 0,
Source nvarchar(80) NOT NULL CONSTRAINT DF_CharacterEnrichmentEvidence_Source DEFAULT N'Character enrichment',
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_CharacterEnrichmentEvidence_CreatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_CharacterEnrichmentEvidence_Run FOREIGN KEY (CharacterEnrichmentRunID) REFERENCES dbo.CharacterEnrichmentRuns(CharacterEnrichmentRunID) ON DELETE CASCADE,
CONSTRAINT FK_CharacterEnrichmentEvidence_Character FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_CharacterEnrichmentEvidence_Scene FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_CharacterEnrichmentEvidence_Character' AND object_id = OBJECT_ID(N'dbo.CharacterEnrichmentEvidence'))
CREATE INDEX IX_CharacterEnrichmentEvidence_Character ON dbo.CharacterEnrichmentEvidence(CharacterID, CharacterEnrichmentRunID, FieldName);
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_Queue
@ProjectID int,
@BookID int,
@UserID int,
@StoryIntelligenceBookPipelineID int = NULL,
@CharacterResolutionVersion nvarchar(120)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @ExistingID int, @TotalCharacters int;
SELECT @TotalCharacters = COUNT(DISTINCT c.CharacterID)
FROM dbo.Characters c
INNER JOIN dbo.SceneCharacters sc ON sc.CharacterID = c.CharacterID
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.IsArchived = 0
INNER JOIN dbo.SceneManuscriptSources sms ON sms.SceneID = s.SceneID
WHERE c.ProjectID = @ProjectID
AND c.IsArchived = 0
AND ch.BookID = @BookID;
SELECT TOP (1) @ExistingID = CharacterEnrichmentRunID
FROM dbo.CharacterEnrichmentRuns
WHERE BookID = @BookID
AND CharacterResolutionVersion = @CharacterResolutionVersion
AND IsCurrent = 1
AND Status IN (N'Queued', N'Running', N'Completed')
ORDER BY CharacterEnrichmentRunID DESC;
IF @ExistingID IS NULL
BEGIN
INSERT dbo.CharacterEnrichmentRuns
(ProjectID, BookID, UserID, StoryIntelligenceBookPipelineID, CharacterResolutionVersion, Status, TotalCharacters, CurrentStage, CurrentMessage)
VALUES
(@ProjectID, @BookID, @UserID, @StoryIntelligenceBookPipelineID, @CharacterResolutionVersion, N'Queued', @TotalCharacters, N'Preparing character analysis', N'Preparing character analysis...');
SET @ExistingID = CAST(SCOPE_IDENTITY() AS int);
END;
SELECT * FROM dbo.CharacterEnrichmentRuns WHERE CharacterEnrichmentRunID = @ExistingID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_Retry
@BookID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @PreviousID int, @ProjectID int, @PipelineID int, @Version nvarchar(120);
SELECT TOP (1)
@PreviousID = CharacterEnrichmentRunID,
@ProjectID = ProjectID,
@PipelineID = StoryIntelligenceBookPipelineID,
@Version = CharacterResolutionVersion
FROM dbo.CharacterEnrichmentRuns
WHERE BookID = @BookID AND UserID = @UserID AND IsCurrent = 1 AND Status = N'Failed'
ORDER BY CharacterEnrichmentRunID DESC;
IF @PreviousID IS NULL
BEGIN
SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns;
RETURN;
END;
UPDATE dbo.CharacterEnrichmentRuns
SET IsCurrent = 0,
Status = N'Superseded',
UpdatedUtc = SYSUTCDATETIME()
WHERE CharacterEnrichmentRunID = @PreviousID;
EXEC dbo.CharacterEnrichmentRun_Queue
@ProjectID = @ProjectID,
@BookID = @BookID,
@UserID = @UserID,
@StoryIntelligenceBookPipelineID = @PipelineID,
@CharacterResolutionVersion = @Version;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_ClaimNext
@LeaseMinutes int = 90
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @RunID int;
SELECT TOP (1) @RunID = CharacterEnrichmentRunID
FROM dbo.CharacterEnrichmentRuns WITH (UPDLOCK, READPAST)
WHERE IsCurrent = 1
AND (
Status = N'Queued'
OR (Status = N'Running' AND StartedUtc < DATEADD(minute, -ABS(@LeaseMinutes), SYSUTCDATETIME()))
)
ORDER BY QueuedUtc, CharacterEnrichmentRunID;
IF @RunID IS NULL
BEGIN
SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns;
RETURN;
END;
UPDATE dbo.CharacterEnrichmentRuns
SET Status = N'Running',
StartedUtc = COALESCE(StartedUtc, SYSUTCDATETIME()),
AttemptCount = AttemptCount + 1,
CurrentStage = N'Preparing character analysis',
CurrentMessage = N'Preparing character analysis...',
UpdatedUtc = SYSUTCDATETIME()
WHERE CharacterEnrichmentRunID = @RunID;
SELECT * FROM dbo.CharacterEnrichmentRuns WHERE CharacterEnrichmentRunID = @RunID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_GetCurrentByBook
@BookID int
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (1) *
FROM dbo.CharacterEnrichmentRuns
WHERE BookID = @BookID AND IsCurrent = 1
ORDER BY CharacterEnrichmentRunID DESC;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_UpdateProgress
@CharacterEnrichmentRunID int,
@TotalCharacters int,
@ProcessedCharacters int,
@CurrentStage nvarchar(120),
@CurrentMessage nvarchar(300)
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.CharacterEnrichmentRuns
SET TotalCharacters = @TotalCharacters,
ProcessedCharacters = @ProcessedCharacters,
CurrentStage = @CurrentStage,
CurrentMessage = @CurrentMessage,
UpdatedUtc = SYSUTCDATETIME()
WHERE CharacterEnrichmentRunID = @CharacterEnrichmentRunID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichment_ContextGet
@ProjectID int,
@BookID int
AS
BEGIN
SET NOCOUNT ON;
SELECT
BookTitle = CASE WHEN NULLIF(LTRIM(RTRIM(b.Subtitle)), N'') IS NULL THEN b.BookTitle ELSE CONCAT(b.BookTitle, N': ', b.Subtitle) END,
b.StoryEra,
SeriesStartDate = CAST(NULL AS date)
FROM dbo.Books b
WHERE b.BookID = @BookID AND b.ProjectID = @ProjectID AND b.IsArchived = 0;
SELECT DISTINCT
c.CharacterID,
c.CharacterName,
c.BirthDate,
c.AgeAtSeriesStart,
c.Height,
c.EyeColour,
c.DefaultDescription
FROM dbo.Characters c
INNER JOIN dbo.SceneCharacters sc ON sc.CharacterID = c.CharacterID
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.IsArchived = 0
INNER JOIN dbo.SceneManuscriptSources sms ON sms.SceneID = s.SceneID
WHERE c.ProjectID = @ProjectID
AND c.IsArchived = 0
AND ch.BookID = @BookID
ORDER BY c.CharacterName;
SELECT alias.CharacterID, alias.Alias
FROM dbo.CharacterAliases alias
INNER JOIN dbo.Characters c ON c.CharacterID = alias.CharacterID
WHERE c.ProjectID = @ProjectID AND c.IsArchived = 0
ORDER BY alias.CharacterID, alias.SortOrder, alias.Alias;
SELECT
sc.CharacterID,
s.SceneID,
ch.ChapterID,
ch.ChapterNumber,
ch.ChapterTitle,
s.SceneNumber,
s.SceneTitle,
sms.SourceText
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID AND c.ProjectID = @ProjectID AND c.IsArchived = 0
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.BookID = @BookID AND ch.IsArchived = 0
INNER JOIN dbo.SceneManuscriptSources sms ON sms.SceneID = s.SceneID
ORDER BY sc.CharacterID, ch.SortOrder, ch.ChapterNumber, s.SortOrder, s.SceneNumber, s.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_Complete
@CharacterEnrichmentRunID int,
@Model nvarchar(100),
@InputTokens int = NULL,
@OutputTokens int = NULL,
@TotalTokens int = NULL,
@DurationMs bigint = NULL,
@ResultsJson nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @ProjectID int, @BookID int, @PipelineID int;
SELECT @ProjectID = ProjectID, @BookID = BookID, @PipelineID = StoryIntelligenceBookPipelineID
FROM dbo.CharacterEnrichmentRuns
WHERE CharacterEnrichmentRunID = @CharacterEnrichmentRunID;
DECLARE @Results TABLE
(
CharacterID int NOT NULL PRIMARY KEY,
Summary nvarchar(max) NULL,
Confidence decimal(5,4) NOT NULL,
DateOfBirthValue nvarchar(120) NULL,
DateOfBirthConfidence decimal(5,4) NULL,
DateOfBirthEvidence nvarchar(700) NULL,
DateOfBirthSceneID int NULL,
AgeValue nvarchar(120) NULL,
AgeConfidence decimal(5,4) NULL,
AgeEvidence nvarchar(700) NULL,
AgeSceneID int NULL,
HeightValue nvarchar(120) NULL,
HeightConfidence decimal(5,4) NULL,
HeightEvidence nvarchar(700) NULL,
HeightSceneID int NULL,
EyeColourValue nvarchar(120) NULL,
EyeColourConfidence decimal(5,4) NULL,
EyeColourEvidence nvarchar(700) NULL,
EyeColourSceneID int NULL
);
INSERT @Results
SELECT
CharacterID,
NULLIF(LTRIM(RTRIM(Summary)), N''),
Confidence,
NULLIF(LTRIM(RTRIM(DateOfBirthValue)), N''),
DateOfBirthConfidence,
NULLIF(LTRIM(RTRIM(DateOfBirthEvidence)), N''),
DateOfBirthSceneID,
NULLIF(LTRIM(RTRIM(AgeValue)), N''),
AgeConfidence,
NULLIF(LTRIM(RTRIM(AgeEvidence)), N''),
AgeSceneID,
NULLIF(LTRIM(RTRIM(HeightValue)), N''),
HeightConfidence,
NULLIF(LTRIM(RTRIM(HeightEvidence)), N''),
HeightSceneID,
NULLIF(LTRIM(RTRIM(EyeColourValue)), N''),
EyeColourConfidence,
NULLIF(LTRIM(RTRIM(EyeColourEvidence)), N''),
EyeColourSceneID
FROM OPENJSON(@ResultsJson)
WITH
(
CharacterID int '$.characterID',
Summary nvarchar(max) '$.summary',
Confidence decimal(5,4) '$.confidence',
DateOfBirthValue nvarchar(120) '$.dateOfBirth.value',
DateOfBirthConfidence decimal(5,4) '$.dateOfBirth.confidence',
DateOfBirthEvidence nvarchar(700) '$.dateOfBirth.evidence',
DateOfBirthSceneID int '$.dateOfBirth.sceneID',
AgeValue nvarchar(120) '$.ageAtStartOfSeries.value',
AgeConfidence decimal(5,4) '$.ageAtStartOfSeries.confidence',
AgeEvidence nvarchar(700) '$.ageAtStartOfSeries.evidence',
AgeSceneID int '$.ageAtStartOfSeries.sceneID',
HeightValue nvarchar(120) '$.height.value',
HeightConfidence decimal(5,4) '$.height.confidence',
HeightEvidence nvarchar(700) '$.height.evidence',
HeightSceneID int '$.height.sceneID',
EyeColourValue nvarchar(120) '$.eyeColour.value',
EyeColourConfidence decimal(5,4) '$.eyeColour.confidence',
EyeColourEvidence nvarchar(700) '$.eyeColour.evidence',
EyeColourSceneID int '$.eyeColour.sceneID'
)
WHERE CharacterID IS NOT NULL;
UPDATE c
SET DefaultDescription = CASE WHEN NULLIF(LTRIM(RTRIM(c.DefaultDescription)), N'') IS NULL THEN r.Summary ELSE c.DefaultDescription END,
BirthDate = CASE WHEN c.BirthDate IS NULL THEN TRY_CONVERT(date, r.DateOfBirthValue) ELSE c.BirthDate END,
AgeAtSeriesStart = CASE WHEN c.AgeAtSeriesStart IS NULL THEN TRY_CONVERT(int, r.AgeValue) ELSE c.AgeAtSeriesStart END,
Height = CASE WHEN NULLIF(LTRIM(RTRIM(c.Height)), N'') IS NULL THEN r.HeightValue ELSE c.Height END,
EyeColour = CASE WHEN NULLIF(LTRIM(RTRIM(c.EyeColour)), N'') IS NULL THEN r.EyeColourValue ELSE c.EyeColour END,
UpdatedDate = SYSUTCDATETIME()
FROM dbo.Characters c
INNER JOIN @Results r ON r.CharacterID = c.CharacterID
WHERE c.ProjectID = @ProjectID;
MERGE dbo.CharacterIntelligenceProfiles WITH (HOLDLOCK) AS target
USING
(
SELECT
ImportSessionID = @PipelineID,
ProjectID = @ProjectID,
BookID = @BookID,
CanonicalIdentityKey = CONCAT(N'character-enrichment-', r.CharacterID),
CanonicalName = c.CharacterName,
ProfileJson = (SELECT r.* FOR JSON PATH, WITHOUT_ARRAY_WRAPPER),
Confidence = r.Confidence,
NarrativeImportance = CAST(NULL AS nvarchar(60))
FROM @Results r
INNER JOIN dbo.Characters c ON c.CharacterID = r.CharacterID
WHERE @PipelineID IS NOT NULL
) AS source
ON target.ImportSessionID = source.ImportSessionID
AND target.CanonicalIdentityKey = source.CanonicalIdentityKey
WHEN MATCHED THEN
UPDATE SET CanonicalName = source.CanonicalName,
ProfileJson = source.ProfileJson,
Confidence = source.Confidence,
EvidenceVersion = N'CharacterEnrichmentV1',
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, CanonicalName, ProfileJson, Confidence, NarrativeImportance, IsRecurring, EvidenceVersion, LastUpdatedSceneResultID)
VALUES (source.ImportSessionID, source.ProjectID, source.BookID, source.CanonicalIdentityKey, source.CanonicalName, source.ProfileJson, source.Confidence, source.NarrativeImportance, 1, N'CharacterEnrichmentV1', NULL);
INSERT dbo.CharacterEnrichmentEvidence (CharacterEnrichmentRunID, CharacterID, SceneID, FieldName, Value, Evidence, Confidence)
SELECT @CharacterEnrichmentRunID, CharacterID, NULL, N'Summary', Summary, NULL, Confidence
FROM @Results
WHERE Summary IS NOT NULL
UNION ALL
SELECT @CharacterEnrichmentRunID, CharacterID, DateOfBirthSceneID, N'DateOfBirth', DateOfBirthValue, DateOfBirthEvidence, COALESCE(DateOfBirthConfidence, 0)
FROM @Results WHERE DateOfBirthValue IS NOT NULL AND DateOfBirthSceneID IS NOT NULL
UNION ALL
SELECT @CharacterEnrichmentRunID, CharacterID, AgeSceneID, N'AgeAtStartOfSeries', AgeValue, AgeEvidence, COALESCE(AgeConfidence, 0)
FROM @Results WHERE AgeValue IS NOT NULL AND AgeSceneID IS NOT NULL
UNION ALL
SELECT @CharacterEnrichmentRunID, CharacterID, HeightSceneID, N'Height', HeightValue, HeightEvidence, COALESCE(HeightConfidence, 0)
FROM @Results WHERE HeightValue IS NOT NULL AND HeightSceneID IS NOT NULL
UNION ALL
SELECT @CharacterEnrichmentRunID, CharacterID, EyeColourSceneID, N'EyeColour', EyeColourValue, EyeColourEvidence, COALESCE(EyeColourConfidence, 0)
FROM @Results WHERE EyeColourValue IS NOT NULL AND EyeColourSceneID IS NOT NULL;
UPDATE dbo.CharacterEnrichmentRuns
SET Status = N'Completed',
ProcessedCharacters = TotalCharacters,
CurrentStage = N'Complete',
CurrentMessage = N'Character summaries are ready.',
CompletedUtc = SYSUTCDATETIME(),
Model = @Model,
InputTokens = @InputTokens,
OutputTokens = @OutputTokens,
TotalTokens = @TotalTokens,
DurationMs = @DurationMs,
ErrorMessage = NULL,
ErrorDetail = NULL,
UpdatedUtc = SYSUTCDATETIME()
WHERE CharacterEnrichmentRunID = @CharacterEnrichmentRunID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_Fail
@CharacterEnrichmentRunID int,
@ErrorMessage nvarchar(max),
@ErrorDetail nvarchar(max) = NULL,
@DurationMs bigint = NULL
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.CharacterEnrichmentRuns
SET Status = N'Failed',
FailedUtc = SYSUTCDATETIME(),
CurrentStage = N'Failed',
CurrentMessage = N'Character enrichment failed.',
ErrorMessage = @ErrorMessage,
ErrorDetail = @ErrorDetail,
DurationMs = @DurationMs,
UpdatedUtc = SYSUTCDATETIME()
WHERE CharacterEnrichmentRunID = @CharacterEnrichmentRunID;
END;
GO