Hardened D&D spell extraction prompts
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user