Gate combat extraction on scene descriptions
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
|
||||
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
||||
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
@@ -59,6 +60,7 @@ func TestProductionCombatPipelineRetriesMergesNormalizesAndWritesJSON(t *testing
|
||||
}
|
||||
fingerprints := prepared.CheckpointFingerprints()
|
||||
assertCombatFingerprint(t, fingerprints, "extract:combat:"+combatextract.Key+":npc_registry")
|
||||
assertCombatFingerprint(t, fingerprints, "extract:combat:"+combatextract.Key+":scene_eligibility")
|
||||
assertCombatFingerprint(t, fingerprints, "normalize:combat:"+combatnormalize.Key+":npc_registry")
|
||||
if len(fingerprints) == 0 {
|
||||
t.Fatal("checkpoint fingerprints = empty, want production combat identities")
|
||||
@@ -99,16 +101,16 @@ func TestProductionCombatPipelineRetriesMergesNormalizesAndWritesJSON(t *testing
|
||||
if !hasCombatWarning(output.Warnings, combatnormalize.ReasonCodeActorCanonicalized) {
|
||||
t.Fatalf("warnings = %#v, want registry normalization warning", output.Warnings)
|
||||
}
|
||||
if len(output.Manifest.References) != 2 {
|
||||
t.Fatalf("manifest references = %#v, want separate extract and normalize provenance", output.Manifest.References)
|
||||
if len(output.Manifest.References) != 3 {
|
||||
t.Fatalf("manifest references = %#v, want scene eligibility plus extract and normalize NPC provenance", output.Manifest.References)
|
||||
}
|
||||
lane := output.Manifest.ArtifactLanes[0]
|
||||
if lane.ID != "combat" || lane.Extractor != combatextract.Key || lane.Merger != pipeline.DefaultMergeModule || lane.Normalizer != combatnormalize.Key {
|
||||
t.Fatalf("manifest lane = %#v, want complete combat composition", lane)
|
||||
}
|
||||
extractorMetadata, ok := lane.Metadata["extractor"].(map[string]any)
|
||||
if !ok || extractorMetadata["npc_count"] != 2 || extractorMetadata["npc_registry_digest"] == "" {
|
||||
t.Fatalf("extractor metadata = %#v, want registry digest and count", lane.Metadata)
|
||||
if !ok || extractorMetadata["npc_count"] != 2 || extractorMetadata["npc_registry_digest"] == "" || extractorMetadata["scene_description_count"] != 3 || extractorMetadata["scene_eligibility_digest"] == "" || extractorMetadata["scene_gate_policy"] != "dnd.combat_turns.scene_gate.v1" {
|
||||
t.Fatalf("extractor metadata = %#v, want registry and scene eligibility metadata", lane.Metadata)
|
||||
}
|
||||
normalizerMetadata, ok := lane.Metadata["normalizer"].(map[string]any)
|
||||
if !ok || normalizerMetadata["npc_count"] != 2 || normalizerMetadata["normalization_policy"] != combatnormalize.NormalizationPolicy {
|
||||
@@ -131,6 +133,13 @@ func TestProductionCombatPipelineAttributesExhaustedInvalidEnumsToShapeValidatio
|
||||
configValue := combatOnlyConfig()
|
||||
profile := configValue.Pipelines["dnd-combat-fixture"]
|
||||
profile.Chunk.Options["max_units"] = 100
|
||||
profile.References["scene_descriptions"] = pipeline.ExternalReference(combatSceneDescriptionPath(t, []dnd.SceneDescription{{
|
||||
ID: "chunk-000001",
|
||||
SourceRef: source.SourceRef{SourceID: "npc-session", StartUnitID: 1, EndUnitID: 5},
|
||||
Kind: dnd.SceneKindCombat,
|
||||
Title: "Complete combat encounter",
|
||||
Summary: "The accepted chunk contains active combat.",
|
||||
}}))
|
||||
configValue.Pipelines["dnd-combat-fixture"] = profile
|
||||
effective, err := configValue.Resolve(config.ResolveInput{
|
||||
PipelineID: "dnd-combat-fixture",
|
||||
@@ -144,7 +153,11 @@ func TestProductionCombatPipelineAttributesExhaustedInvalidEnumsToShapeValidatio
|
||||
combatTestInvalidEnumResponse("invalid"),
|
||||
combatTestInvalidEnumResponse("unknown"),
|
||||
}}
|
||||
prepared, err := pipeline.Prepare(effective.ResolvedPipeline, registries, pipeline.ModuleDependencies{LLM: client})
|
||||
materialized, _, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, moduleCatalog(registries), pipeline.ReferenceMaterializationOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("MaterializeReferences() error = %v", err)
|
||||
}
|
||||
prepared, err := pipeline.Prepare(materialized, registries, pipeline.ModuleDependencies{LLM: client})
|
||||
if err != nil {
|
||||
t.Fatalf("Prepare() error = %v, want nil", err)
|
||||
}
|
||||
@@ -267,11 +280,25 @@ func combatTestNPCPayload(t *testing.T) combatNPCPayload {
|
||||
return combatNPCPayload{path: path}
|
||||
}
|
||||
|
||||
func combatSceneDescriptionPath(t *testing.T, scenes []dnd.SceneDescription) string {
|
||||
t.Helper()
|
||||
content, err := scenecodec.New().Encode(dnd.SceneDescriptionList{Scenes: scenes})
|
||||
if err != nil {
|
||||
t.Fatalf("Encode(scene descriptions) error = %v", err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "scene-descriptions.json")
|
||||
if err := os.WriteFile(path, content, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func combatOnlyConfig() config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.Pipelines["dnd-combat"] = pipeline.PipelineProfile{
|
||||
Input: pipeline.Binding(transcript.Key),
|
||||
Chunk: pipeline.ModuleBinding{Module: pipeline.DefaultChunkModule, Options: map[string]any{"max_units": 2}},
|
||||
Input: pipeline.Binding(transcript.Key),
|
||||
Chunk: pipeline.ModuleBinding{Module: pipeline.DefaultChunkModule, Options: map[string]any{"max_units": 2}},
|
||||
References: map[string]pipeline.ReferenceSource{"scene_descriptions": pipeline.ExternalReference(repositoryPathForIntegration("internal", "modules", "integration", "testdata", "combat_scene_descriptions.json"))},
|
||||
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"combat": {
|
||||
Extract: pipeline.ModuleBinding{Module: combatextract.Key, Retries: 2},
|
||||
|
||||
@@ -16,9 +16,11 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
|
||||
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
|
||||
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
||||
sceneextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/scenedescriptions"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||
)
|
||||
|
||||
@@ -41,8 +43,9 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
|
||||
t.Fatalf("Prepare() error = %v", err)
|
||||
}
|
||||
for name, value := range map[string]string{
|
||||
"extract:npcs:dnd/npcs:mapping_policy": "dnd.npcs.extract_mapping.v2",
|
||||
"extract:spells:dnd/spells:mapping_policy": "dnd.spells.extract_mapping.v2",
|
||||
"extract:npcs:dnd/npcs:mapping_policy": "dnd.npcs.extract_mapping.v2",
|
||||
"extract:spells:dnd/spells:mapping_policy": "dnd.spells.extract_mapping.v2",
|
||||
"extract:combat:dnd/combat-turns:scene_gate_policy": "dnd.combat_turns.scene_gate.v1",
|
||||
} {
|
||||
assertFingerprintValue(t, prepared.CheckpointFingerprints(), name, value)
|
||||
}
|
||||
@@ -54,6 +57,7 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
|
||||
"extract:spells:dnd/spells:npc_registry",
|
||||
"extract:combat:dnd/combat-turns:prompt",
|
||||
"extract:combat:dnd/combat-turns:response_schema",
|
||||
"extract:combat:dnd/combat-turns:scene_eligibility",
|
||||
"extract:combat:dnd/combat-turns:npc_registry",
|
||||
"normalize:combat:dnd/combat-turns:npc_registry",
|
||||
} {
|
||||
@@ -67,19 +71,20 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 3 {
|
||||
t.Fatalf("run outputs = %#v rejected = %#v, want NPC, spell, and combat outputs", output.NormalizeOutputs, output.Rejected)
|
||||
if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 4 {
|
||||
t.Fatalf("run outputs = %#v rejected = %#v, want NPC, scene, spell, and combat outputs", output.NormalizeOutputs, output.Rejected)
|
||||
}
|
||||
wantSchemas := map[string]string{"npcs": npccodec.SchemaID, "spells": spellcodec.SchemaID, "combat": combatcodec.SchemaID}
|
||||
wantSchemas := map[string]string{"npcs": npccodec.SchemaID, "scene-descriptions": scenecodec.SchemaID, "spells": spellcodec.SchemaID, "combat": combatcodec.SchemaID}
|
||||
for _, serialized := range output.NormalizeOutputs {
|
||||
if serialized.Artifact.Schema.ID != wantSchemas[serialized.LaneID] || serialized.Artifact.Schema.Version != "v1" {
|
||||
t.Fatalf("%s artifact schema = %#v, want minimal v1 identity", serialized.LaneID, serialized.Artifact.Schema)
|
||||
}
|
||||
}
|
||||
wantExtractorIdentity := map[string]struct{ promptID, schemaID string }{
|
||||
"npcs": {npcs.PromptID, npcs.ResponseSchemaID},
|
||||
"spells": {spells.PromptID, spells.ResponseSchemaID},
|
||||
"combat": {combatextract.PromptID, combatextract.ResponseSchemaID},
|
||||
"npcs": {npcs.PromptID, npcs.ResponseSchemaID},
|
||||
"scene-descriptions": {sceneextract.PromptID, sceneextract.ResponseSchemaID},
|
||||
"spells": {spells.PromptID, spells.ResponseSchemaID},
|
||||
"combat": {combatextract.PromptID, combatextract.ResponseSchemaID},
|
||||
}
|
||||
for _, lane := range output.Manifest.ArtifactLanes {
|
||||
want, ok := wantExtractorIdentity[lane.ID]
|
||||
@@ -138,6 +143,15 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
|
||||
if provenanceCount != 3 {
|
||||
t.Fatalf("NPC generated provenance count = %d, want spell extract plus combat extract/normalize", provenanceCount)
|
||||
}
|
||||
sceneProvenanceCount := 0
|
||||
for _, reference := range output.Manifest.References {
|
||||
if reference.SlotName == "scene_descriptions" {
|
||||
sceneProvenanceCount++
|
||||
}
|
||||
}
|
||||
if sceneProvenanceCount != 1 {
|
||||
t.Fatalf("scene generated provenance count = %d, want combat extractor handoff", sceneProvenanceCount)
|
||||
}
|
||||
manifestContent, err := json.Marshal(output.Manifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -150,7 +164,7 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
|
||||
for _, lane := range output.Manifest.ArtifactLanes {
|
||||
for _, component := range []string{"extractor", "normalizer"} {
|
||||
metadata, ok := lane.Metadata[component].(map[string]any)
|
||||
if ok && metadata["npc_registry_digest"] != nil {
|
||||
if ok && (metadata["npc_registry_digest"] != nil || metadata["scene_eligibility_digest"] != nil) {
|
||||
t.Fatalf("%s %s metadata = %#v, want generated identity only in framework provenance", lane.ID, component, metadata)
|
||||
}
|
||||
}
|
||||
@@ -235,6 +249,8 @@ func (client *groundedDNDLLMClient) CompleteStructured(ctx context.Context, requ
|
||||
"name": "Hooded Guard", "source_refs": []any{map[string]int{"start_unit_id": 3, "end_unit_id": 3}},
|
||||
},
|
||||
}}
|
||||
case sceneextract.PromptID:
|
||||
payload = map[string]any{"kind": "combat", "title": "A combat encounter", "summary": "The party faces an active encounter."}
|
||||
case spells.PromptID:
|
||||
payload = map[string]any{"spell_casts": []any{map[string]any{
|
||||
"caster": "Mira Thorn", "spell": "Cure Wounds",
|
||||
|
||||
37
internal/modules/integration/testdata/combat_scene_descriptions.json
vendored
Normal file
37
internal/modules/integration/testdata/combat_scene_descriptions.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"scenes": [
|
||||
{
|
||||
"id": "chunk-000001",
|
||||
"source_ref": {
|
||||
"source_id": "npc-session",
|
||||
"start_unit_id": 1,
|
||||
"end_unit_id": 2
|
||||
},
|
||||
"kind": "combat",
|
||||
"title": "First combat exchange",
|
||||
"summary": "The first accepted chunk contains active combat."
|
||||
},
|
||||
{
|
||||
"id": "chunk-000002",
|
||||
"source_ref": {
|
||||
"source_id": "npc-session",
|
||||
"start_unit_id": 3,
|
||||
"end_unit_id": 4
|
||||
},
|
||||
"kind": "combat",
|
||||
"title": "Second combat exchange",
|
||||
"summary": "The second accepted chunk contains active combat."
|
||||
},
|
||||
{
|
||||
"id": "chunk-000003",
|
||||
"source_ref": {
|
||||
"source_id": "npc-session",
|
||||
"start_unit_id": 5,
|
||||
"end_unit_id": 5
|
||||
},
|
||||
"kind": "combat",
|
||||
"title": "Final combat exchange",
|
||||
"summary": "The final accepted chunk contains active combat."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,12 +16,19 @@ pipelines:
|
||||
npcs:
|
||||
extract: dnd/npcs
|
||||
normalize: dnd/npcs
|
||||
scene-descriptions:
|
||||
extract: dnd/scene-descriptions
|
||||
normalize: dnd/scene-descriptions
|
||||
- id: grounded-events
|
||||
references:
|
||||
npcs:
|
||||
artifact:
|
||||
step: identify-npcs
|
||||
lane: npcs
|
||||
scene_descriptions:
|
||||
artifact:
|
||||
step: identify-npcs
|
||||
lane: scene-descriptions
|
||||
artifacts:
|
||||
spells:
|
||||
extract: dnd/spells
|
||||
|
||||
Reference in New Issue
Block a user