Hardened D&D spell extraction prompts

This commit is contained in:
2026-07-20 20:25:23 -05:00
parent fa5076f5f1
commit c4c907d421
7 changed files with 130 additions and 32 deletions

View File

@@ -2,9 +2,27 @@ Source references must use integer source-unit IDs from the transcript. Provide
start_unit_id and end_unit_id for each source reference; the extractor assigns
source_id automatically.
For each artifact, its source references must collectively support every
factual claim in caster, spell, effect, and narrative_description. Include the
casting declaration or action and every later transcript unit needed to support
reported targets, saving throws, damage, healing, conditions, interruptions,
and outcomes. When supporting evidence is non-contiguous, provide multiple
narrow source references. Do not use one broad range merely to bridge unrelated
table conversation. If a detail is not supported by the cited transcript
units, omit that detail or describe only the supported attempt or declaration.
Return only D&D spell-cast artifacts. For each spell cast, identify the in-world
caster, spell name, effect, narrative description, and source references.
The caster must be the canonical in-world character or creature, not the human
player, transcript speaker, or GM. Use the player and party references together
with transcript context to map first-person player speech to the associated
player character and use the canonical character name from the references.
Likewise, attribute a spell narrated by the GM to the in-world creature that
casts it. Never return a player or GM name when the associated in-world caster
can be identified. If the caster cannot be resolved, use only the most specific
in-world identity supported by the transcript; do not invent a name.
Use the canonical spell-name catalog to select spell names. Do not return a
spell name absent from that catalog, even when it is suggested by general D&D
knowledge or reference material.
@@ -13,4 +31,11 @@ Use player, party, and glossary reference material only to clarify source text;
references are not source evidence for a spell cast. Do not return spells,
casters, or effects that are mentioned only in reference material.
Effects and narrative descriptions are session records, not rules summaries.
Report only mechanics, explanations, and outcomes established by the cited
transcript units. Preserve the table's observed resolution without silently
correcting it from general D&D knowledge. If the transcript gives a possibly
nonstandard rationale, use wording such as "the GM rules" or "the table
resolves" rather than asserting that rationale as a universal rule.
Return exactly one JSON object and no explanatory text.

View File

@@ -4,6 +4,13 @@ Extract only spell casts that are supported by the transcript. Do not infer
spells from general D&D knowledge or from table chatter that does not identify a
spell being cast.
Describe the session as it was played and adjudicated. The transcript is
authoritative for what happened in this session, even when a table ruling may
differ from published D&D rules. Do not correct the transcript or fill in
unstated mechanics from general D&D knowledge. When a ruling or mechanical
explanation matters, attribute it to the GM or table instead of presenting it
as a universal game rule.
Use the provided canonical spell-name catalog when naming each extracted spell.
Return the canonical catalog spelling exactly. The catalog is a recognition
aid; it does not establish that a spell was cast.
aid; it does not establish that a spell was cast or how the spell works.

View File

@@ -20,23 +20,28 @@
"properties": {
"caster": {
"type": "string",
"minLength": 1
"minLength": 1,
"description": "Canonical in-world character or creature that casts the spell, never the human player, transcript speaker, or GM when the in-world caster can be identified."
},
"spell": {
"type": "string",
"minLength": 1
"minLength": 1,
"description": "Canonical spell name from the provided spell-name catalog."
},
"effect": {
"type": "string",
"minLength": 1
"minLength": 1,
"description": "Concise effect or resolution established by the cited transcript units; do not infer mechanics from general D&D rules knowledge."
},
"narrative_description": {
"type": "string",
"minLength": 1
"minLength": 1,
"description": "Short session-grounded description containing only details established by the cited transcript units."
},
"source_refs": {
"type": "array",
"minItems": 1,
"description": "One or more narrow transcript ranges that collectively support every factual claim in this spell-cast object, including any reported resolution or outcome.",
"items": {
"type": "object",
"additionalProperties": false,

View File

@@ -51,6 +51,8 @@ type Extractor struct {
llm contracts.StructuredLLMClient
effectiveCatalog spellcatalog.EffectiveCatalog
catalogPromptInput contracts.LLMInputMaterial
promptSHA string
responseSchemaSHA string
}
func New(llmClient contracts.StructuredLLMClient, _ Options, references ...contracts.ReferenceSet) (*Extractor, error) {
@@ -72,10 +74,20 @@ func New(llmClient contracts.StructuredLLMClient, _ Options, references ...contr
if err != nil {
return nil, extractorErrorf("prepare spell catalog prompt input: %w", err)
}
promptSHA, err := scriptoriumPromptMetadata()
if err != nil {
return nil, extractorErrorf("load prompt metadata: %w", err)
}
responseSchema, err := loadResponseSchema()
if err != nil {
return nil, extractorErrorf("load response schema: %w", err)
}
return &Extractor{
llm: llmClient,
effectiveCatalog: effectiveCatalog,
catalogPromptInput: catalogPromptInput,
promptSHA: promptSHA,
responseSchemaSHA: responseSchema.SHA256,
}, nil
}
@@ -88,24 +100,18 @@ func (e *Extractor) ReferenceSlots() []contracts.ReferenceSlot {
}
func (e *Extractor) ManifestMetadata() map[string]any {
promptSHA, err := scriptoriumPromptMetadata()
if err != nil {
promptSHA = ""
}
metadata := map[string]any{
"prompt_id": PromptID,
"prompt_version": SchemaVersion,
"prompt_sha256": promptSHA,
"catalog_base_id": e.effectiveCatalog.BaseID(),
"catalog_digest": e.effectiveCatalog.Digest(),
"catalog_overlay_ids": e.effectiveCatalog.OverlayIDs(),
"response_schema_key": string(ResponseSchemaKey),
"response_schema_id": ResponseSchemaID,
"response_schema_name": ResponseSchemaName,
}
if schema, err := loadResponseSchema(); err == nil {
metadata["response_schema_version"] = schema.Version
metadata["response_schema_sha256"] = schema.SHA256
"prompt_id": PromptID,
"prompt_version": SchemaVersion,
"prompt_sha256": e.promptSHA,
"catalog_base_id": e.effectiveCatalog.BaseID(),
"catalog_digest": e.effectiveCatalog.Digest(),
"catalog_overlay_ids": e.effectiveCatalog.OverlayIDs(),
"response_schema_key": string(ResponseSchemaKey),
"response_schema_id": ResponseSchemaID,
"response_schema_name": ResponseSchemaName,
"response_schema_version": SchemaVersion,
"response_schema_sha256": e.responseSchemaSHA,
}
return metadata
}
@@ -114,7 +120,11 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
if e == nil {
return nil
}
return []pipeline.CheckpointFingerprint{{Name: "effective_catalog", Value: e.effectiveCatalog.Digest()}}
return []pipeline.CheckpointFingerprint{
{Name: "effective_catalog", Value: e.effectiveCatalog.Digest()},
{Name: "prompt", Value: e.promptSHA},
{Name: "response_schema", Value: e.responseSchemaSHA},
}
}
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.SpellList], error) {

View File

@@ -118,8 +118,18 @@ func TestExtractPromptUsesCanonicalOverlayNamesWithoutAliasesOrMetadata(t *testi
t.Fatalf("catalog overlay metadata = %#v", metadata["catalog_overlay_ids"])
}
fingerprints := newExtractor(t, &fakeSpellsLLMClient{}, overlaySpellCatalogReference()).CheckpointFingerprints()
if len(fingerprints) != 1 || fingerprints[0].Name != "effective_catalog" || fingerprints[0].Value != metadata["catalog_digest"] {
t.Fatalf("checkpoint fingerprints = %#v, want manifest catalog digest %#v", fingerprints, metadata["catalog_digest"])
wantFingerprints := map[string]any{
"effective_catalog": metadata["catalog_digest"],
"prompt": metadata["prompt_sha256"],
"response_schema": metadata["response_schema_sha256"],
}
if len(fingerprints) != len(wantFingerprints) {
t.Fatalf("checkpoint fingerprints = %#v, want prompt, response schema, and catalog identities", fingerprints)
}
for _, fingerprint := range fingerprints {
if want, ok := wantFingerprints[fingerprint.Name]; !ok || fingerprint.Value != want {
t.Fatalf("checkpoint fingerprint %q = %q, want %#v", fingerprint.Name, fingerprint.Value, want)
}
}
encoded, err := json.Marshal(metadata)
if err != nil {