Consolidate scene and normalization prompt instructions
This commit is contained in:
6
assets/dnd/locations/normalize/prompts/instructions.md
Normal file
6
assets/dnd/locations/normalize/prompts/instructions.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Use candidate names and their cited transcript windows to determine whether
|
||||
candidates identify the same physical place. Do not treat matching names,
|
||||
nearby evidence, nested places, or generic labels as sufficient. Keep parent
|
||||
and child places, similarly named places, and uncertain aliases separate.
|
||||
|
||||
When selecting a canonical display name, prefer the clearest established name.
|
||||
@@ -12,7 +12,7 @@ messages:
|
||||
- role: system
|
||||
content_file: ./sharedassets/common-dnd-system.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./instructions.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||
cache_control:
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
Review location candidates and cited transcript context. Group candidates only
|
||||
when the evidence clearly identifies one physical place.
|
||||
|
||||
Do not group candidates solely because their names match, their evidence is
|
||||
nearby, one place is nested inside another, or their labels are generic. Keep
|
||||
parent and child places, similarly named places, and uncertain aliases
|
||||
separate. For an accepted group, select the supplied candidate with the
|
||||
clearest established display name as canonical.
|
||||
10
assets/dnd/npcs/normalize/prompts/instructions.md
Normal file
10
assets/dnd/npcs/normalize/prompts/instructions.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Use candidate aliases and their cited transcript windows to determine whether
|
||||
candidates refer to the same individual. Preserve distinct individuals even
|
||||
when their names are similar.
|
||||
|
||||
When selecting a canonical display name, prefer a complete, stable proper name
|
||||
over an abbreviation. Prefer an unadorned proper name over that name plus a
|
||||
contextual class, role, title, or relationship descriptor unless the transcript
|
||||
establishes the descriptor as part of the person's name. A longer display name
|
||||
is not inherently more canonical; for example, do not prefer `Captain Aria`
|
||||
over `Aria` solely because it includes the contextual title `Captain`.
|
||||
@@ -12,7 +12,7 @@ messages:
|
||||
- role: system
|
||||
content_file: ./sharedassets/common-dnd-system.md
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
content_file: ./instructions.md
|
||||
- role: user
|
||||
content_file: ./sharedassets/common-dnd-entity-reconciliation.md
|
||||
cache_control:
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
Review NPC candidates and their cited transcript context to identify aliases
|
||||
that refer to the same individual. Propose only groups supported by the
|
||||
transcript, and preserve distinct individuals even when their names are
|
||||
similar.
|
||||
|
||||
For every accepted group, choose as canonical only a supplied candidate from
|
||||
that evidence-supported duplicate group. Prefer a complete, stable proper name
|
||||
over an abbreviation. Prefer an unadorned proper name over that name plus a
|
||||
contextual class, role, title, or relationship descriptor unless the transcript
|
||||
establishes the descriptor as part of the person's name. A longer display name
|
||||
is not inherently more canonical; for example, do not prefer `Captain Aria`
|
||||
over `Aria` solely because it includes the contextual title `Captain`. Do not
|
||||
invent, edit, or combine display names.
|
||||
@@ -1,4 +1,9 @@
|
||||
Choose exactly one kind:
|
||||
Describe exactly one accepted Dungeons & Dragons scene from the supplied
|
||||
transcript chunk. The complete chunk is the evidence boundary: do not split it
|
||||
into multiple scenes or use facts that are not supported by it.
|
||||
|
||||
Return one kind, one concise title, and one concise summary. Choose exactly one
|
||||
kind:
|
||||
|
||||
- combat: active combat materially organizes the scene, including
|
||||
initiative-like exchanges or sustained hostile action. Planning a fight or
|
||||
@@ -39,6 +44,3 @@ outcome established within the accepted chunk. Do not add analysis, inferred
|
||||
motives, hidden state, future consequences, relationship claims, or facts from
|
||||
outside the chunk. Campaign references may disambiguate names but never add
|
||||
events or lore.
|
||||
|
||||
Do not return identifiers, source identifiers, source ranges, unit identifiers,
|
||||
participants, confidence, or any fields besides kind, title, and summary.
|
||||
|
||||
@@ -27,8 +27,6 @@ messages:
|
||||
content_file: ./sharedassets/common-dnd-transcript-chunk.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
cache_control:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Describe exactly one accepted Dungeons & Dragons scene from the supplied
|
||||
transcript chunk. The complete chunk is the evidence boundary: do not split it
|
||||
into multiple scenes or use facts that are not supported by it.
|
||||
|
||||
Return one kind, one concise title, and one concise summary.
|
||||
@@ -1,8 +1,19 @@
|
||||
Cover the complete provided transcript from its first source unit to its last
|
||||
source unit. Return scenes in source-unit order with no gaps or overlaps. Use
|
||||
only positive integer source-unit IDs from the transcript, and give every scene
|
||||
one inclusive `start_unit_id` and one inclusive `end_unit_id`.
|
||||
Divide the complete provided transcript into coherent Dungeons & Dragons scenes
|
||||
for the `dnd/scenes` chunk module.
|
||||
|
||||
Return exactly one JSON object and no explanatory text. The object must contain
|
||||
only a non-empty `scenes` array. Each scene object must contain only
|
||||
`start_unit_id` and `end_unit_id`.
|
||||
A scene is a coherent unit of play. Start a new scene when the transcript
|
||||
establishes a meaningful change in location, objective, threat, activity,
|
||||
encounter, or mode of play. Good reasons include a material move, beginning or
|
||||
ending combat, a substantially different encounter phase, a shift between
|
||||
combat, exploration, social interaction, planning, travel, rest, or downtime,
|
||||
a change in the central NPC, faction, threat, or objective, or a sustained
|
||||
table-level interruption that materially changes the activity.
|
||||
|
||||
Do not split a scene merely because a speaker or combat round changes, a
|
||||
routine turn occurs, or the table briefly digresses. Prefer fewer coherent
|
||||
scenes over speculative or fine-grained boundaries.
|
||||
|
||||
Cover the complete transcript from its first source unit to its last. Return
|
||||
scenes in source-unit order with no gaps or overlaps. Use only positive integer
|
||||
source-unit IDs from the transcript, and give every scene one inclusive
|
||||
`start_unit_id` and one inclusive `end_unit_id`.
|
||||
|
||||
@@ -21,8 +21,6 @@ messages:
|
||||
content_file: ./sharedassets/common-dnd-references.md
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content_file: ./task.md
|
||||
- role: user
|
||||
content_file: ./instructions.md
|
||||
- role: user
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
Divide the provided transcript into coherent Dungeons & Dragons scenes for the
|
||||
`dnd/scenes` chunk module.
|
||||
|
||||
A scene is a coherent unit of play. Start a new scene when the transcript
|
||||
establishes a meaningful change in location, objective, threat, activity,
|
||||
encounter, or mode of play. Good reasons include a material move, beginning or
|
||||
ending combat, a substantially different encounter phase, a shift between
|
||||
combat, exploration, social interaction, planning, travel, rest, or downtime,
|
||||
a change in the central NPC, faction, threat, or objective, or a sustained
|
||||
table-level interruption that materially changes the activity.
|
||||
|
||||
Do not split a scene merely because a speaker or combat round changes, a
|
||||
routine turn occurs, or the table briefly digresses. Prefer fewer coherent
|
||||
scenes over speculative or fine-grained boundaries.
|
||||
|
||||
Return only inclusive `start_unit_id` and `end_unit_id` endpoints for each
|
||||
scene. Do not return titles, modes, participants, summaries, boundary notes,
|
||||
confidence, caveats, final chunk IDs, or chunk indexes.
|
||||
@@ -17,7 +17,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: "dnd.scenes",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
|
||||
{Name: "task.md", Path: "prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
||||
},
|
||||
SharedFiles: []string{
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
func TestPromptAssetsPrepareTranscriptAndTaskMessages(t *testing.T) {
|
||||
func TestPromptAssetsPrepareTranscriptWithInstructionOrdering(t *testing.T) {
|
||||
if !slices.Contains(promptAssetManifest.SharedFiles, "common-dnd-transcript-full.md") {
|
||||
t.Fatalf("shared prompt assets = %#v, want complete-session transcript scope", promptAssetManifest.SharedFiles)
|
||||
}
|
||||
@@ -22,35 +22,42 @@ func TestPromptAssetsPrepareTranscriptAndTaskMessages(t *testing.T) {
|
||||
if prepared.PromptID != PromptID {
|
||||
t.Fatalf("prompt id = %q, want %q", prepared.PromptID, PromptID)
|
||||
}
|
||||
if len(prepared.Messages) != 5 {
|
||||
t.Fatalf("prepared messages = %d, want 5", len(prepared.Messages))
|
||||
if len(prepared.Messages) < 4 {
|
||||
t.Fatalf("prepared messages = %#v, want system, references, instructions, and transcript", prepared.Messages)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if prepared.Messages[0].Role != "system" || prepared.Messages[1].Role != "user" {
|
||||
t.Fatalf("initial prompt roles = %#v, want system followed by campaign references", prepared.Messages[:2])
|
||||
}
|
||||
for _, index := range []int{1, 4} {
|
||||
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||
t.Errorf("message %d cache control = %#v, want ephemeral", index, cache)
|
||||
}
|
||||
if cache := prepared.Messages[1].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||
t.Fatalf("campaign-reference cache control = %#v, want ephemeral", cache)
|
||||
}
|
||||
for _, index := range []int{0, 2, 3} {
|
||||
if cache := prepared.Messages[index].CacheControl; cache != nil {
|
||||
t.Errorf("message %d cache control = %#v, want nil", index, cache)
|
||||
}
|
||||
if cache := prepared.Messages[0].CacheControl; cache != nil {
|
||||
t.Fatalf("system cache control = %#v, want nil", cache)
|
||||
}
|
||||
if references := prepared.Messages[1].Content; !strings.Contains(references, "scene-players") || !strings.Contains(references, "scene-party") || !strings.Contains(references, "scene-glossary") {
|
||||
t.Fatalf("reference message = %q, want supplied reference inputs", references)
|
||||
}
|
||||
if transcriptMessage := prepared.Messages[4].Content; !strings.Contains(transcriptMessage, "scene-transcript") {
|
||||
t.Fatalf("final message = %q, want transcript input", transcriptMessage)
|
||||
transcriptIndex := -1
|
||||
for index, message := range prepared.Messages {
|
||||
if strings.Contains(message.Content, "scene-transcript") {
|
||||
transcriptIndex = index
|
||||
}
|
||||
}
|
||||
if transcriptIndex != len(prepared.Messages)-1 {
|
||||
t.Fatalf("transcript message index = %d, want final message in %#v", transcriptIndex, prepared.Messages)
|
||||
}
|
||||
if cache := prepared.Messages[transcriptIndex].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||
t.Fatalf("transcript cache control = %#v, want ephemeral", cache)
|
||||
}
|
||||
instructionsIndex := transcriptIndex - 1
|
||||
if instructionsIndex <= 1 || prepared.Messages[instructionsIndex].CacheControl != nil {
|
||||
t.Fatalf("instruction message = %#v, want uncached message after references and before transcript", prepared.Messages[instructionsIndex])
|
||||
}
|
||||
for index, message := range prepared.Messages {
|
||||
if index != 1 && (strings.Contains(message.Content, "scene-players") || strings.Contains(message.Content, "scene-party") || strings.Contains(message.Content, "scene-glossary")) {
|
||||
t.Errorf("message %d unexpectedly rendered reference input", index)
|
||||
}
|
||||
if index != 4 && strings.Contains(message.Content, "scene-transcript") {
|
||||
if index != transcriptIndex && strings.Contains(message.Content, "scene-transcript") {
|
||||
t.Errorf("message %d unexpectedly rendered transcript input", index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: "dnd.scene_descriptions",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
|
||||
{Name: "task.md", Path: "prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
||||
},
|
||||
SharedFiles: []string{
|
||||
|
||||
@@ -30,8 +30,8 @@ func TestRegisterPromptAssetsPreparesSceneDescriptionPrompt(t *testing.T) {
|
||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "scene-description-test-profile",
|
||||
Inputs: map[string]promptkit.ArtifactRef{
|
||||
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[1]}`),
|
||||
"players": promptkit.Inline(" "),
|
||||
"transcript": promptkit.InlineWithURI("file:///session.json", `{"sentinel":"scene-description-transcript"}`),
|
||||
"players": promptkit.Inline("scene-description-player"),
|
||||
"party": promptkit.Inline(" "),
|
||||
"glossary": promptkit.Inline(" "),
|
||||
},
|
||||
@@ -42,6 +42,36 @@ func TestRegisterPromptAssetsPreparesSceneDescriptionPrompt(t *testing.T) {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_scene_descriptions_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v, want scene-description prompt identity and schema wiring", prepared)
|
||||
}
|
||||
if len(prepared.Messages) < 5 {
|
||||
t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages)
|
||||
}
|
||||
referenceIndex := -1
|
||||
transcriptIndex := -1
|
||||
for index, message := range prepared.Messages {
|
||||
if strings.Contains(message.Content, "scene-description-player") {
|
||||
referenceIndex = index
|
||||
}
|
||||
if strings.Contains(message.Content, "scene-description-transcript") {
|
||||
transcriptIndex = index
|
||||
}
|
||||
}
|
||||
instructionsIndex := len(prepared.Messages) - 1
|
||||
if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex {
|
||||
t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex)
|
||||
}
|
||||
for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} {
|
||||
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||
t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache)
|
||||
}
|
||||
}
|
||||
for index, message := range prepared.Messages {
|
||||
if index != referenceIndex && strings.Contains(message.Content, "scene-description-player") {
|
||||
t.Errorf("message %d unexpectedly rendered campaign-reference input", index)
|
||||
}
|
||||
if index != transcriptIndex && strings.Contains(message.Content, "scene-description-transcript") {
|
||||
t.Errorf("message %d unexpectedly rendered transcript input", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptMetadataAndDiagnosticsDoNotContainRawAssets(t *testing.T) {
|
||||
|
||||
@@ -17,7 +17,7 @@ var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: PromptID,
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
|
||||
{Name: "task.md", Path: "prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
||||
{Name: "candidates.md", Path: "prompts/candidates.md"},
|
||||
},
|
||||
SharedFiles: []string{"common-dnd-system.md", "common-dnd-entity-reconciliation.md", "common-dnd-transcript-windows.md"},
|
||||
|
||||
@@ -12,6 +12,18 @@ import (
|
||||
)
|
||||
|
||||
func TestRegisterPromptAssetsPreparesLocationNormalizationPrompt(t *testing.T) {
|
||||
hasInstructions := false
|
||||
for _, file := range promptAssetManifest.ModuleFiles {
|
||||
if file.Name == "task.md" {
|
||||
t.Fatalf("module prompt files = %#v, want no retired task asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
if file.Name == "instructions.md" {
|
||||
hasInstructions = true
|
||||
}
|
||||
}
|
||||
if !hasInstructions {
|
||||
t.Fatalf("module prompt files = %#v, want instructions asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
registry := llm.NewAssetRegistry()
|
||||
if err := entityreconcile.RegisterSchemaAssets(registry); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -32,15 +44,26 @@ func TestRegisterPromptAssetsPreparesLocationNormalizationPrompt(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" || len(prepared.Messages) != 5 {
|
||||
if prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" || len(prepared.Messages) < 5 {
|
||||
t.Fatalf("prepared prompt = %#v", prepared)
|
||||
}
|
||||
for _, index := range []int{2, 4} {
|
||||
if prepared.Messages[index].CacheControl == nil {
|
||||
if prepared.Messages[index].CacheControl == nil || prepared.Messages[index].CacheControl.Type != promptkit.CacheControlEphemeral {
|
||||
t.Fatalf("message %d cache = %#v", index, prepared.Messages[index].CacheControl)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(prepared.Messages[3].Content, "candidate-000001") || strings.Contains(prepared.Messages[3].Content, `"windows"`) {
|
||||
t.Fatalf("candidate message = %q", prepared.Messages[3].Content)
|
||||
}
|
||||
if !strings.Contains(prepared.Messages[4].Content, `"windows"`) || strings.Contains(prepared.Messages[4].Content, "candidate-000001") {
|
||||
t.Fatalf("transcript message = %q", prepared.Messages[4].Content)
|
||||
}
|
||||
for index, message := range prepared.Messages {
|
||||
if index != 3 && strings.Contains(message.Content, "candidate-000001") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: PromptID,
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
|
||||
{Name: "task.md", Path: "prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
||||
{Name: "candidates.md", Path: "prompts/candidates.md"},
|
||||
},
|
||||
SharedFiles: []string{
|
||||
|
||||
@@ -16,6 +16,18 @@ func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) {
|
||||
if want := []string{"common-dnd-system.md", "common-dnd-entity-reconciliation.md", "common-dnd-transcript-windows.md"}; !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) {
|
||||
t.Fatalf("shared prompt assets = %#v, want %#v", promptAssetManifest.SharedFiles, want)
|
||||
}
|
||||
hasInstructions := false
|
||||
for _, file := range promptAssetManifest.ModuleFiles {
|
||||
if file.Name == "task.md" {
|
||||
t.Fatalf("module prompt files = %#v, want no retired task asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
if file.Name == "instructions.md" {
|
||||
hasInstructions = true
|
||||
}
|
||||
}
|
||||
if !hasInstructions {
|
||||
t.Fatalf("module prompt files = %#v, want instructions asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
if promptHash, err := promptAssetMetadata(); err != nil || promptHash == "" {
|
||||
t.Fatalf("promptAssetMetadata() = %q, %v; want prompt fingerprint", promptHash, err)
|
||||
}
|
||||
@@ -50,13 +62,11 @@ func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v, want normalization prompt identity and schema", prepared)
|
||||
}
|
||||
if len(prepared.Messages) != 5 {
|
||||
t.Fatalf("prepared messages = %d, want 5", len(prepared.Messages))
|
||||
if len(prepared.Messages) < 5 {
|
||||
t.Fatalf("prepared messages = %#v, want system, instructions, reconciliation policy, candidates, and transcript windows", prepared.Messages)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if prepared.Messages[0].Role != "system" {
|
||||
t.Fatalf("initial message role = %q, want system", prepared.Messages[0].Role)
|
||||
}
|
||||
for _, index := range []int{2, 4} {
|
||||
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
||||
|
||||
Reference in New Issue
Block a user