From d3c4d6f133a622a2cc6227f01e9abf343a3d4d9c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 26 Jul 2026 02:40:08 +0000 Subject: [PATCH] Simplify NPC normalization prompt guidance --- docs/internal/llm.md | 22 ++++++------ .../assets/prompts/dnd.npcs.normalize.yaml | 2 -- .../dnd/normalize/npcs/scriptorium_assets.go | 1 - .../normalize/npcs/scriptorium_assets_test.go | 36 ++++++++++++++----- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/docs/internal/llm.md b/docs/internal/llm.md index d208888..5d264e2 100644 --- a/docs/internal/llm.md +++ b/docs/internal/llm.md @@ -157,18 +157,18 @@ messages remain canonical shared assets rather than copied package text. ### D&D NPC Normalization Prompt Ordering And Cache Boundaries NPC normalization has a distinct prompt and response-schema identity from NPC -extraction. Its stable message tiers are the common D&D system and identity -assets, followed by package-owned task and normalization instructions. Cache -boundaries follow the shared identity tier and the package instructions. The -variable tail contains the private candidate-name-and-range input and a -windowed transcript input whose cited units provide local context; neither has -a cache boundary because it changes with the document. +extraction. Its stable message tiers are the common D&D system asset, followed +by package-owned task and normalization instructions. Cache boundaries follow +the shared system tier and the package instructions. The variable tail contains +the private candidate-name-and-range input and a windowed transcript input +whose cited units provide local context; neither has a cache boundary because +it changes with the document. -This prompt intentionally omits extraction-evidence and campaign-reference -assets: it reconciles existing records rather than extracting events or adding -evidence. Its package-owned manifest and schema identity are fingerprinted -separately, so a normalization prompt or schema change cannot reuse a prior -normalization checkpoint. +This prompt intentionally omits shared identity guidance, +extraction-evidence, and campaign-reference assets: it reconciles existing +records rather than extracting events or adding evidence. Its package-owned +manifest and schema identity are fingerprinted separately, so a normalization +prompt or schema change cannot reuse a prior normalization checkpoint. Shared wording belongs in the canonical assets under `internal/modules/dnd/shared`; extraction packages reference those assets in diff --git a/internal/modules/dnd/normalize/npcs/assets/prompts/dnd.npcs.normalize.yaml b/internal/modules/dnd/normalize/npcs/assets/prompts/dnd.npcs.normalize.yaml index 88610f0..6e7b12a 100644 --- a/internal/modules/dnd/normalize/npcs/assets/prompts/dnd.npcs.normalize.yaml +++ b/internal/modules/dnd/normalize/npcs/assets/prompts/dnd.npcs.normalize.yaml @@ -11,8 +11,6 @@ inputs: messages: - role: system content_file: ./sharedassets/common-dnd-system.md - - role: user - content_file: ./sharedassets/common-dnd-identity.md cache_control: type: ephemeral - role: user diff --git a/internal/modules/dnd/normalize/npcs/scriptorium_assets.go b/internal/modules/dnd/normalize/npcs/scriptorium_assets.go index e840aab..3a38ef7 100644 --- a/internal/modules/dnd/normalize/npcs/scriptorium_assets.go +++ b/internal/modules/dnd/normalize/npcs/scriptorium_assets.go @@ -21,7 +21,6 @@ var promptAssetManifest = shared.PromptAssetManifest{ }, SharedFiles: []string{ "common-dnd-system.md", - "common-dnd-identity.md", "common-dnd-transcript.md", }, } diff --git a/internal/modules/dnd/normalize/npcs/scriptorium_assets_test.go b/internal/modules/dnd/normalize/npcs/scriptorium_assets_test.go index d72a7c7..70f45bd 100644 --- a/internal/modules/dnd/normalize/npcs/scriptorium_assets_test.go +++ b/internal/modules/dnd/normalize/npcs/scriptorium_assets_test.go @@ -2,6 +2,7 @@ package npcs import ( "context" + "reflect" "strings" "testing" "time" @@ -11,6 +12,12 @@ import ( ) func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) { + if want := []string{"common-dnd-system.md", "common-dnd-transcript.md"}; !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) { + t.Fatalf("shared prompt assets = %#v, want %#v", promptAssetManifest.SharedFiles, want) + } + if promptHash, err := scriptoriumPromptMetadata(); err != nil || promptHash == "" { + t.Fatalf("scriptoriumPromptMetadata() = %q, %v; want prompt fingerprint", promptHash, err) + } registry := llm.NewAssetRegistry() if err := RegisterPromptAssets(registry); err != nil { t.Fatalf("RegisterPromptAssets() error = %v", err) @@ -39,23 +46,36 @@ func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) { if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npcs_normalize_llm.v1.json" { t.Fatalf("prepared prompt = %#v, want normalization prompt identity and schema", prepared) } - if len(prepared.Messages) != 6 { - t.Fatalf("prepared messages = %d, want 6", len(prepared.Messages)) + if len(prepared.Messages) != 5 { + t.Fatalf("prepared messages = %d, want 5", len(prepared.Messages)) } - for _, index := range []int{1, 3} { + for index, role := range []string{"system", "user", "user", "user", "user"} { + if prepared.Messages[index].Role != role { + t.Errorf("message %d role = %q, want %q", index, prepared.Messages[index].Role, role) + } + } + for _, index := range []int{0, 2} { if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != scriptorium.CacheControlEphemeral { t.Errorf("message %d cache control = %#v, want ephemeral", index, cache) } } - for _, index := range []int{0, 2, 4, 5} { + for _, index := range []int{1, 3, 4} { if cache := prepared.Messages[index].CacheControl; cache != nil { t.Errorf("message %d cache control = %#v, want nil", index, cache) } } - if !strings.Contains(prepared.Messages[4].Content, `"Mira"`) || strings.Contains(prepared.Messages[4].Content, `"windows"`) { - t.Fatalf("candidate message = %q, want only rendered candidates", prepared.Messages[4].Content) + if !strings.Contains(prepared.Messages[3].Content, `"Mira"`) || strings.Contains(prepared.Messages[3].Content, `"windows"`) { + t.Fatalf("candidate message = %q, want only rendered candidates", prepared.Messages[3].Content) } - if !strings.Contains(prepared.Messages[5].Content, `"windows"`) || strings.Contains(prepared.Messages[5].Content, `"Mira"`) { - t.Fatalf("transcript message = %q, want only rendered transcript", prepared.Messages[5].Content) + if !strings.Contains(prepared.Messages[4].Content, `"windows"`) || strings.Contains(prepared.Messages[4].Content, `"Mira"`) { + t.Fatalf("transcript message = %q, want only rendered transcript", prepared.Messages[4].Content) + } + for index, message := range prepared.Messages { + if index != 3 && strings.Contains(message.Content, `"Mira"`) { + t.Errorf("message %d unexpectedly rendered candidate input", index) + } + if index != 4 && strings.Contains(message.Content, `"windows"`) { + t.Errorf("message %d unexpectedly rendered transcript input", index) + } } }