Consolidate grounded event extraction instructions

This commit is contained in:
2026-08-05 14:55:40 +00:00
parent 16d1b29b14
commit 4b52cace76
15 changed files with 141 additions and 112 deletions

View File

@@ -1,14 +1,25 @@
Extract Dungeons & Dragons enemy events from the supplied combat transcript.
An `engaged` event requires direct establishment that a subject is actively
opposing the party in combat. A `killed`, `fled`, `captured`, or
`incapacitated` event requires explicit establishment of that outcome. An
outcome may share evidence with an engagement, and a later engagement or
outcome for the same subject remains a separate observation. Emit at most one
`engaged` observation for the same subject in this combat scene.
For `killed`, direct death or killing is required. For `fled`, the subject
must explicitly escape, retreat, or leave combat to avoid continued engagement.
For `captured`, the subject must be explicitly taken prisoner or secured
under the party's control. For `incapacitated`, the subject must be explicitly
unable to continue acting without being established as killed or captured.
When the transcript identifies a named NPC, use its normalized registry
spelling. A hostile creature without a registry entry is allowed. For unnamed
individuals or groups, use only the narrowest transcript-grounded label, such
as `Orcs`, `One orc`, or `Remaining orcs`; never invent member names, IDs,
or quantities.
Exclude party members, allies, neutral observers, mentioned-but-absent enemies,
hazards, traps, environmental effects, uncertain allegiance, table talk,
planning, hypotheses, recaps outside this passage, and downstream inference.
Do not infer an engagement or outcome from initiative, turn absence, damage,
defeat, movement, a scene ending, combat-opponent grounding, or any auxiliary
artifact. Auxiliary inputs can guide attention but cannot prove or supply an
event. Cite only narrow current-transcript ranges that establish each event.
Return the `events` array even when no enemy event is established. Every event
must contain only `name`, `kind`, and `source_refs`. Use exactly one kind:
`engaged`, `killed`, `fled`, `captured`, or `incapacitated`. Each source range
uses integer `start_unit_id` and `end_unit_id`; omit `source_id` because
Notarius assigns the current source identity.
defeat, movement, or a scene ending.

View File

@@ -42,8 +42,6 @@ messages:
content_file: ./sharedassets/common-dnd-npcs.md
- role: user
content_file: ./combat-grounding.md
- role: user
content_file: ./task.md
- role: user
content_file: ./instructions.md
cache_control:

View File

@@ -1,20 +0,0 @@
Extract Dungeons & Dragons enemy events from the supplied combat transcript.
Return an `engaged` event only when the transcript directly establishes that a
subject is actively opposing the party in combat. Return `killed`, `fled`,
`captured`, or `incapacitated` only when the transcript explicitly establishes
that outcome. An outcome may share evidence with an engagement, and a later
engagement or outcome for the same subject remains a separate observation.
Emit at most one engagement for the same subject in this combat scene.
For `killed`, direct death or killing is required. For `fled`, the subject must
explicitly escape, retreat, or leave combat to avoid continued engagement. For
`captured`, the subject must be explicitly taken prisoner or secured under the
party's control. For `incapacitated`, the subject must be explicitly unable to
continue acting without being established as killed or captured.
Use a normalized NPC registry spelling when the transcript identifies that
named NPC. A hostile creature without a registry entry is allowed. For unnamed
individuals or groups, use only the narrowest transcript-grounded label, such
as `Orcs`, `One orc`, or `Remaining orcs`; never invent member names, IDs, or
quantities.

View File

@@ -1,8 +1,24 @@
Return the occurrences array even when no occurrence is established. Every
record must contain location_id, name, kind, and source_refs. Copy location_id
and name from one supplied registry record, and cite only narrow transcript
ranges that support both that location and its classified occurrence.
Extract Dungeons & Dragons location occurrences from the supplied transcript.
Include an occurrence only when the transcript establishes one supplied
location, one occurrence kind, and a coherent passage supporting both.
Do not summarize location descriptions, infer a missing registry record, or
use registry context as evidence. Omit source_id; Notarius assigns the current
transcript source identity.
Use exactly one kind per occurrence:
- visited: party members are physically present, arrive, remain, or depart;
- planned: the party explicitly proposes, intends, or agrees to future travel;
- recalled: the transcript explicitly recounts an earlier party visit; or
- mentioned: the location is explicitly referenced without stronger support,
including non-actionable speculation or a mere hypothetical reference.
A mere hypothetical or speculative reference is not planned unless the
transcript also establishes an actual proposal, intention, or agreement to
travel. When the hypothetical explicitly names a supplied location, it may be
mentioned.
For overlapping support, visited outranks planned, recalled, and mentioned;
planned outranks recalled and mentioned; recalled outranks mentioned. A passage
may produce multiple records when it independently establishes separate facts,
such as recalling an earlier visit while planning a return. Omit inferred,
unstated, uncertain, or unsupported places and occurrences. Do not infer a
location or occurrence from surrounding events when the transcript does not
state it. Do not summarize location descriptions.

View File

@@ -34,8 +34,6 @@ messages:
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./location-registry.md
- role: user
content_file: ./task.md
- role: user
content_file: ./instructions.md
cache_control:

View File

@@ -1,29 +0,0 @@
Extract Dungeons & Dragons location occurrences from the supplied transcript.
Include an occurrence only when the transcript establishes one supplied
location, one occurrence kind, and a coherent passage supporting both. Use
only the exact ID and name pair from the supplied location registry. Return an
empty occurrences array when no supplied location has an evidenced occurrence
in this transcript passage.
Use exactly one kind per occurrence:
- visited: party members are physically present, arrive, remain, or depart;
- planned: the party explicitly proposes, intends, or agrees to future travel;
- recalled: the transcript explicitly recounts an earlier party visit; or
- mentioned: the location is explicitly referenced without stronger support,
including non-actionable speculation or a mere hypothetical reference.
A mere hypothetical or speculative reference is not planned unless the
transcript also establishes an actual proposal, intention, or agreement to
travel. When the hypothetical itself explicitly names a supplied registry
location, it may be mentioned using the narrow passage that supports that
reference.
For overlapping support, visited outranks planned, recalled, and mentioned;
planned outranks recalled and mentioned; recalled outranks mentioned. A passage
may produce multiple records when it independently establishes separate facts,
such as recalling an earlier visit while planning a return. Omit inferred,
unstated, uncertain, or unsupported places and occurrences. Do not infer a
location or occurrence from surrounding events when the transcript does not
state it.

View File

@@ -1,6 +1,13 @@
Return the interactions array even when no interaction is established. Every
record must contain name, kind, and source_refs. Cite transcript ranges that
support both the NPC identity and the interaction kind.
Extract Dungeons & Dragons NPC interaction occurrences from the supplied
transcript. Include an occurrence only when the transcript establishes one
supplied NPC, one interaction kind, and a coherent passage supporting both.
Use only names from the supplied NPC registry.
Do not summarize, infer relationships, sentiment, factions, motives, aliases,
or persistent state. Do not identify player characters, anonymous groups, or
invented NPCs. Keep occurrences within this transcript chunk. Split records
when an NPC's interaction kind changes, when combat alignment changes, or when
an NPC is first mentioned and later becomes present.
Use exactly one kind per occurrence:

View File

@@ -34,8 +34,6 @@ messages:
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./sharedassets/common-dnd-npcs.md
- role: user
content_file: ./task.md
- role: user
content_file: ./instructions.md
cache_control:

View File

@@ -1,16 +0,0 @@
Extract Dungeons & Dragons NPC interaction occurrences from the supplied
transcript.
Include an occurrence only when the transcript establishes one supplied NPC,
one interaction kind, and a coherent passage supporting both. Use only names
from the supplied NPC registry. The registry helps ground identity but never
proves that an interaction occurred.
Do not summarize, infer relationships, sentiment, factions, motives, aliases,
or persistent state. Do not identify player characters, anonymous groups, or
invented NPCs. Return an empty interactions array when no supplied NPC has an
evidenced interaction in this transcript passage.
Keep occurrences within this transcript chunk. Split records when an NPC's
interaction kind changes, when combat alignment changes, or when an NPC is
first mentioned and later becomes present.

View File

@@ -18,7 +18,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "combat-grounding.md", Path: "prompts/combat-grounding.md"},
{Name: "task.md", Path: "prompts/task.md"},
{Name: "instructions.md", Path: "prompts/instructions.md"},
},
SharedFiles: []string{

View File

@@ -28,21 +28,32 @@ func TestRegisterPromptAssetsAndPrepareEnemyEventPrompt(t *testing.T) {
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_enemy_events_llm.v1.json" || prepared.SelectedProfileID != "dnd-extraction" {
t.Fatalf("prepared prompt = %#v", prepared)
}
referenceIndex := renderedMessageIndex(t, prepared.Messages, "enemy-player")
transcriptIndex := renderedMessageIndex(t, prepared.Messages, "enemy-transcript")
for _, sentinel := range []string{"enemy-npc", "enemy-turn", "enemy-opponent", "Extract Dungeons & Dragons enemy events"} {
if transcriptIndex <= referenceIndex {
t.Fatalf("transcript message index = %d, want after references index %d", transcriptIndex, referenceIndex)
}
groundingIndices := make([]int, 0, 3)
for _, sentinel := range []string{"enemy-npc", "enemy-turn", "enemy-opponent"} {
if index := renderedMessageIndex(t, prepared.Messages, sentinel); index <= transcriptIndex {
t.Fatalf("message containing %q has index %d, want after transcript index %d", sentinel, index, transcriptIndex)
} else {
groundingIndices = append(groundingIndices, index)
}
}
instructionIndex := renderedMessageIndex(t, prepared.Messages, "Return the `events` array")
if instructionIndex <= transcriptIndex {
t.Fatalf("instruction index = %d, want after transcript index %d", instructionIndex, transcriptIndex)
instructionIndex := renderedMessageIndex(t, prepared.Messages, "Extract Dungeons & Dragons enemy events")
for _, index := range groundingIndices {
if instructionIndex <= index {
t.Fatalf("instruction index = %d, want after grounding index %d", instructionIndex, index)
}
}
if instructionIndex != len(prepared.Messages)-1 {
t.Fatalf("instruction message index = %d, want final message", instructionIndex)
}
if cache := prepared.Messages[instructionIndex].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
t.Fatalf("final instruction cache control = %#v", cache)
for _, index := range []int{referenceIndex, transcriptIndex, instructionIndex} {
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache)
}
}
}

View File

@@ -18,7 +18,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "location-registry.md", Path: "prompts/location-registry.md"},
{Name: "task.md", Path: "prompts/task.md"},
{Name: "instructions.md", Path: "prompts/instructions.md"},
},
SharedFiles: []string{

View File

@@ -29,8 +29,8 @@ func TestRegisterPromptAssetsPreparesLocationOccurrencePrompt(t *testing.T) {
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "location-occurrences-test",
Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.Inline(`{"units":[1]}`), "players": promptkit.Inline(" "), "party": promptkit.Inline(" "), "glossary": promptkit.Inline(" "),
"locations": promptkit.Inline(`{"locations":[{"id":"location:sha256:test","name":"The Mill"}]}`),
"transcript": promptkit.Inline(`{"units":[{"sentinel":"location-occurrence-transcript"}]}`), "players": promptkit.Inline("location-occurrence-player"), "party": promptkit.Inline(" "), "glossary": promptkit.Inline(" "),
"locations": promptkit.Inline(`{"locations":[{"id":"location:sha256:test","name":"location-occurrence-registry"}]}`),
},
})
if err != nil {
@@ -40,10 +40,17 @@ func TestRegisterPromptAssetsPreparesLocationOccurrencePrompt(t *testing.T) {
t.Fatalf("prepared prompt = %#v", prepared)
}
var registryMessage string
referenceIndex := -1
transcriptIndex := -1
registryIndex := -1
evidenceIndex := -1
taskIndex := -1
for index, message := range prepared.Messages {
if strings.Contains(message.Content, "location-occurrence-player") {
referenceIndex = index
}
if strings.Contains(message.Content, "location-occurrence-transcript") {
transcriptIndex = index
}
if strings.Contains(message.Content, "normalized location registry") {
registryMessage = message.Content
registryIndex = index
@@ -51,14 +58,28 @@ func TestRegisterPromptAssetsPreparesLocationOccurrencePrompt(t *testing.T) {
if strings.Contains(message.Content, "Transcript units are the only evidence") {
evidenceIndex = index
}
if strings.Contains(message.Content, "Extract Dungeons & Dragons location occurrences") {
taskIndex = index
}
}
if !strings.Contains(registryMessage, "location:sha256:test") || !strings.Contains(registryMessage, "The Mill") || strings.Contains(registryMessage, "source_refs") {
if !strings.Contains(registryMessage, "location:sha256:test") || !strings.Contains(registryMessage, "location-occurrence-registry") || strings.Contains(registryMessage, "source_refs") {
t.Fatalf("rendered prompt did not preserve source-free registry grounding: %s", registryMessage)
}
if evidenceIndex < 0 || taskIndex < 0 || registryIndex <= evidenceIndex || registryIndex >= taskIndex {
t.Fatalf("registry prompt placement = evidence %d, registry %d, task %d", evidenceIndex, registryIndex, taskIndex)
instructionsIndex := len(prepared.Messages) - 1
if referenceIndex < 0 || transcriptIndex <= referenceIndex || evidenceIndex <= transcriptIndex || registryIndex <= evidenceIndex || instructionsIndex <= registryIndex {
t.Fatalf("message order = references %d, transcript %d, evidence %d, registry %d, instructions %d; want that order", referenceIndex, transcriptIndex, evidenceIndex, registryIndex, 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, "location-occurrence-player") {
t.Errorf("message %d unexpectedly rendered campaign-reference input", index)
}
if index != transcriptIndex && strings.Contains(message.Content, "location-occurrence-transcript") {
t.Errorf("message %d unexpectedly rendered transcript input", index)
}
if index != registryIndex && strings.Contains(message.Content, "location-occurrence-registry") {
t.Errorf("message %d unexpectedly rendered registry grounding input", index)
}
}
}

View File

@@ -17,7 +17,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
ModuleDir: "dnd.npc_interactions",
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{

View File

@@ -34,15 +34,15 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
transcript := `{"units":[1]}`
transcript := `{"units":[{"sentinel":"interaction-transcript"}]}`
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "npc-interactions-test-profile",
Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.InlineWithURI("file:///session.json", transcript),
"players": promptkit.Inline("Dana: Mira"),
"players": promptkit.Inline("interaction-player"),
"party": promptkit.Inline("Mira: ranger"),
"glossary": promptkit.Inline("Greencloak: title"),
"npcs": promptkit.Inline(`{"npcs":[{"name":"Mira Thorn"}]}`),
"npcs": promptkit.Inline(`{"npcs":[{"name":"interaction-npc"}]}`),
},
})
if err != nil {
@@ -51,6 +51,43 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) {
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npc_interactions_llm.v1.json" {
t.Fatalf("prepared prompt = %#v", prepared)
}
if len(prepared.Messages) < 6 {
t.Fatalf("prepared messages = %#v, want shared context, NPC grounding, and instructions", prepared.Messages)
}
referenceIndex := -1
transcriptIndex := -1
npcIndex := -1
for index, message := range prepared.Messages {
if strings.Contains(message.Content, "interaction-player") {
referenceIndex = index
}
if strings.Contains(message.Content, "interaction-transcript") {
transcriptIndex = index
}
if strings.Contains(message.Content, "interaction-npc") {
npcIndex = index
}
}
instructionsIndex := len(prepared.Messages) - 1
if referenceIndex < 0 || transcriptIndex <= referenceIndex || npcIndex <= transcriptIndex || instructionsIndex <= npcIndex {
t.Fatalf("message order = references %d, transcript %d, NPCs %d, instructions %d; want that order", referenceIndex, transcriptIndex, npcIndex, 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, "interaction-player") {
t.Errorf("message %d unexpectedly rendered campaign-reference input", index)
}
if index != transcriptIndex && strings.Contains(message.Content, "interaction-transcript") {
t.Errorf("message %d unexpectedly rendered transcript input", index)
}
if index != npcIndex && strings.Contains(message.Content, "interaction-npc") {
t.Errorf("message %d unexpectedly rendered NPC grounding input", index)
}
}
}
func TestPromptMetadataDoesNotExposeAssetContent(t *testing.T) {