Compose production D&D combat pipeline
This commit is contained in:
@@ -4,16 +4,24 @@ package register
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/chunk/scenes"
|
||||
combatcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
|
||||
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
|
||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
||||
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
||||
combatinvariants "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/invariants"
|
||||
combatshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/shape"
|
||||
combatsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_refs"
|
||||
combatrelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_relatedness"
|
||||
npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/identity"
|
||||
npcshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/shape"
|
||||
npcsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcs/source_refs"
|
||||
@@ -42,19 +50,28 @@ func Register(registries pipeline.Registries, assets *llm.AssetRegistry) error {
|
||||
}{
|
||||
{name: "spells codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, codec) }},
|
||||
{name: "npcs codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, npccodec.New()) }},
|
||||
{name: "combat turns codec", register: func() error { return pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, combatcodec.New()) }},
|
||||
{name: "scenes chunker", register: func() error { return scenes.Register(registries.Chunkers) }},
|
||||
{name: "spells extractor", register: func() error { return spells.Register(registries.Extractors) }},
|
||||
{name: "npcs extractor", register: func() error { return npcextract.Register(registries.Extractors) }},
|
||||
{name: "combat turns extractor", register: func() error { return combatextract.Register(registries.Extractors) }},
|
||||
{name: "spell-list appendorder merger", register: func() error {
|
||||
return appendorder.RegisterTyped(registries.Mergers, dnd.SpellListKind, appendSpellLists)
|
||||
}},
|
||||
{name: "npc-list appendorder merger", register: func() error {
|
||||
return appendorder.RegisterTyped(registries.Mergers, dnd.NPCListKind, appendNPCLists)
|
||||
}},
|
||||
{name: "combat-turn-list appendorder merger", register: func() error {
|
||||
return appendorder.RegisterTyped(registries.Mergers, dnd.CombatTurnListKind, appendCombatTurnLists)
|
||||
}},
|
||||
{name: "spells normalizer", register: func() error { return spellnormalize.Register(registries.Normalizers) }},
|
||||
{name: "npcs normalizer", register: func() error { return npcnormalize.Register(registries.Normalizers) }},
|
||||
{name: "combat turns normalizer", register: func() error { return combatnormalize.Register(registries.Normalizers) }},
|
||||
{name: "spell-list noop normalizer", register: func() error { return noop.RegisterTyped[dnd.SpellList](registries.Normalizers, dnd.SpellListKind) }},
|
||||
{name: "npc-list noop normalizer", register: func() error { return noop.RegisterTyped[dnd.NPCList](registries.Normalizers, dnd.NPCListKind) }},
|
||||
{name: "combat-turn-list noop normalizer", register: func() error {
|
||||
return noop.RegisterTyped[dnd.CombatTurnList](registries.Normalizers, dnd.CombatTurnListKind)
|
||||
}},
|
||||
{name: "spell shape validator", register: func() error { return spellshape.Register(registries.Validators) }},
|
||||
{name: "spell catalog validator", register: func() error { return spellcatalog.Register(registries.Validators) }},
|
||||
{name: "spell source references validator", register: func() error { return spellsourcerefs.Register(registries.Validators) }},
|
||||
@@ -63,6 +80,10 @@ func Register(registries pipeline.Registries, assets *llm.AssetRegistry) error {
|
||||
{name: "npc identity validator", register: func() error { return npcidentity.Register(registries.Validators) }},
|
||||
{name: "npc source references validator", register: func() error { return npcsourcerefs.Register(registries.Validators) }},
|
||||
{name: "npc source relatedness validator", register: func() error { return npcrelatedness.Register(registries.Validators) }},
|
||||
{name: "combat shape validator", register: func() error { return combatshape.Register(registries.Validators) }},
|
||||
{name: "combat source references validator", register: func() error { return combatsourcerefs.Register(registries.Validators) }},
|
||||
{name: "combat source relatedness validator", register: func() error { return combatrelatedness.Register(registries.Validators) }},
|
||||
{name: "combat normalized invariants validator", register: func() error { return combatinvariants.Register(registries.Validators) }},
|
||||
{name: "spell-list always accept validator", register: func() error {
|
||||
return alwaysaccept.RegisterTyped[dnd.SpellList](registries.Validators, dnd.SpellListKind)
|
||||
}},
|
||||
@@ -75,9 +96,16 @@ func Register(registries pipeline.Registries, assets *llm.AssetRegistry) error {
|
||||
{name: "npc-list always reject validator", register: func() error {
|
||||
return alwaysreject.RegisterTyped[dnd.NPCList](registries.Validators, dnd.NPCListKind)
|
||||
}},
|
||||
{name: "combat-turn-list always accept validator", register: func() error {
|
||||
return alwaysaccept.RegisterTyped[dnd.CombatTurnList](registries.Validators, dnd.CombatTurnListKind)
|
||||
}},
|
||||
{name: "combat-turn-list always reject validator", register: func() error {
|
||||
return alwaysreject.RegisterTyped[dnd.CombatTurnList](registries.Validators, dnd.CombatTurnListKind)
|
||||
}},
|
||||
{name: "scenes prompt assets", register: func() error { return scenes.RegisterPromptAssets(assets) }},
|
||||
{name: "spells prompt assets", register: func() error { return spells.RegisterPromptAssets(assets) }},
|
||||
{name: "npcs prompt assets", register: func() error { return npcextract.RegisterPromptAssets(assets) }},
|
||||
{name: "combat turns prompt assets", register: func() error { return combatextract.RegisterPromptAssets(assets) }},
|
||||
}
|
||||
for _, registration := range registrations {
|
||||
if err := registration.register(); err != nil {
|
||||
@@ -139,6 +167,33 @@ func Register(registries pipeline.Registries, assets *llm.AssetRegistry) error {
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register dnd npcs normalize validator chain: %w", err)
|
||||
}
|
||||
if err := registries.ValidatorChains.Register(pipeline.ValidatorChainMapping{
|
||||
Stage: pipeline.StageExtract,
|
||||
Module: combatextract.Key,
|
||||
Validators: []pipeline.ModuleBinding{
|
||||
pipeline.Binding(validjson.Key),
|
||||
pipeline.Binding(validjsonschema.Key),
|
||||
pipeline.Binding(combatshape.Key),
|
||||
pipeline.Binding(combatsourcerefs.Key),
|
||||
pipeline.Binding(combatrelatedness.Key),
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register dnd combat turns validator chain: %w", err)
|
||||
}
|
||||
if err := registries.ValidatorChains.Register(pipeline.ValidatorChainMapping{
|
||||
Stage: pipeline.StageNormalize,
|
||||
Module: combatnormalize.Key,
|
||||
Validators: []pipeline.ModuleBinding{
|
||||
pipeline.Binding(validjson.Key),
|
||||
pipeline.Binding(validjsonschema.Key),
|
||||
pipeline.Binding(combatshape.Key),
|
||||
pipeline.Binding(combatinvariants.Key),
|
||||
pipeline.Binding(combatsourcerefs.Key),
|
||||
pipeline.Binding(combatrelatedness.Key),
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register dnd combat turns normalize validator chain: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,6 +228,52 @@ func appendNPCLists(values []dnd.NPCList) (dnd.NPCList, error) {
|
||||
return combined, nil
|
||||
}
|
||||
|
||||
func appendCombatTurnLists(values []dnd.CombatTurnList) (dnd.CombatTurnList, error) {
|
||||
count := 0
|
||||
present := false
|
||||
for _, value := range values {
|
||||
if value.CombatTurns != nil {
|
||||
present = true
|
||||
}
|
||||
count += len(value.CombatTurns)
|
||||
}
|
||||
if !present {
|
||||
return dnd.CombatTurnList{}, nil
|
||||
}
|
||||
combined := dnd.CombatTurnList{CombatTurns: make([]dnd.CombatTurn, 0, count)}
|
||||
for _, value := range values {
|
||||
for _, turn := range value.CombatTurns {
|
||||
combined.CombatTurns = append(combined.CombatTurns, cloneCombatTurn(turn))
|
||||
}
|
||||
}
|
||||
return combined, nil
|
||||
}
|
||||
|
||||
func cloneCombatTurn(value dnd.CombatTurn) dnd.CombatTurn {
|
||||
clone := value
|
||||
if value.Round != nil {
|
||||
round := *value.Round
|
||||
clone.Round = &round
|
||||
}
|
||||
if value.Actions != nil {
|
||||
clone.Actions = make([]dnd.CombatAction, len(value.Actions))
|
||||
for index, action := range value.Actions {
|
||||
clone.Actions[index] = action
|
||||
if action.Targets != nil {
|
||||
clone.Actions[index].Targets = append([]string(nil), action.Targets...)
|
||||
}
|
||||
if action.Resolution != nil {
|
||||
resolution := *action.Resolution
|
||||
clone.Actions[index].Resolution = &resolution
|
||||
}
|
||||
}
|
||||
}
|
||||
if value.SourceRefs != nil {
|
||||
clone.SourceRefs = append([]source.SourceRef(nil), value.SourceRefs...)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func validateRegistries(registries pipeline.Registries, assets *llm.AssetRegistry) error {
|
||||
switch {
|
||||
case registries.Chunkers == nil:
|
||||
|
||||
@@ -7,12 +7,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
|
||||
npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||
combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
|
||||
npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcs"
|
||||
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
||||
)
|
||||
@@ -24,12 +27,13 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
||||
t.Fatalf("Register() error = %v, want nil", err)
|
||||
}
|
||||
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
|
||||
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key})
|
||||
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, pipeline.DefaultNormalizeModule})
|
||||
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind})
|
||||
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind})
|
||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind})
|
||||
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key})
|
||||
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, pipeline.DefaultNormalizeModule})
|
||||
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind})
|
||||
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind})
|
||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind})
|
||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCListKind})
|
||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
||||
assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
|
||||
"extract/dnd/npcs/shape",
|
||||
"extract/dnd/npcs/source_refs",
|
||||
@@ -39,6 +43,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
||||
"extract/dnd/spells/shape",
|
||||
"extract/dnd/spells/source_refs",
|
||||
"extract/dnd/spells/source_relatedness",
|
||||
"extract/dnd/combat-turns/shape",
|
||||
"extract/dnd/combat-turns/source_refs",
|
||||
"extract/dnd/combat-turns/source_relatedness",
|
||||
"normalize/dnd/combat-turns/invariants",
|
||||
"generic/always_accept",
|
||||
"generic/always_reject",
|
||||
})
|
||||
@@ -77,6 +85,27 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
||||
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, npcnormalize.Key); !reflect.DeepEqual(got, npcNormalizeChain) {
|
||||
t.Fatalf("NPC normalize validator chain = %#v, want %#v", got, npcNormalizeChain)
|
||||
}
|
||||
combatExtractChain := []pipeline.ModuleBinding{
|
||||
pipeline.Binding("generic/valid_json"),
|
||||
pipeline.Binding("generic/valid_json_schema"),
|
||||
pipeline.Binding("extract/dnd/combat-turns/shape"),
|
||||
pipeline.Binding("extract/dnd/combat-turns/source_refs"),
|
||||
pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
|
||||
}
|
||||
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, combatextract.Key); !reflect.DeepEqual(got, combatExtractChain) {
|
||||
t.Fatalf("combat extract validator chain = %#v, want %#v", got, combatExtractChain)
|
||||
}
|
||||
combatNormalizeChain := []pipeline.ModuleBinding{
|
||||
pipeline.Binding("generic/valid_json"),
|
||||
pipeline.Binding("generic/valid_json_schema"),
|
||||
pipeline.Binding("extract/dnd/combat-turns/shape"),
|
||||
pipeline.Binding("normalize/dnd/combat-turns/invariants"),
|
||||
pipeline.Binding("extract/dnd/combat-turns/source_refs"),
|
||||
pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
|
||||
}
|
||||
if got := registries.ValidatorChains.Validators(pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, combatNormalizeChain) {
|
||||
t.Fatalf("combat normalize validator chain = %#v, want %#v", got, combatNormalizeChain)
|
||||
}
|
||||
if got := registries.ValidatorChains.Validators(pipeline.StageMerge, npcextract.Key); got != nil {
|
||||
t.Fatalf("NPC merge validator chain = %#v, want absent", got)
|
||||
}
|
||||
@@ -99,11 +128,18 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
||||
"dnd.npcs/sharedassets/common-dnd-system.md",
|
||||
"dnd.npcs/sharedassets/common-dnd-transcript.md",
|
||||
"dnd.npcs/task.md",
|
||||
"dnd.combat_turns/dnd.combat_turns.yaml",
|
||||
"dnd.combat_turns/instructions.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-references.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-system.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-transcript.md",
|
||||
"dnd.combat_turns/task.md",
|
||||
})
|
||||
assertAssetNamesContain(t, assets.SchemaFS, []string{
|
||||
"dnd_scenes.v1.json",
|
||||
"dnd_spells_llm.v1.json",
|
||||
"dnd_npcs_llm.v1.json",
|
||||
"dnd_combat_turns_llm.v1.json",
|
||||
})
|
||||
if spec, ok := registries.Chunkers.Spec("dnd/scenes"); !ok || spec.Key != "dnd/scenes" {
|
||||
t.Fatalf("scene chunker spec = %#v, present = %t; want family-owned spec", spec, ok)
|
||||
@@ -120,6 +156,12 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
||||
if spec, ok := registries.Normalizers.Spec(npcnormalize.Key); !ok || spec.ArtifactKind != dnd.NPCListKind || spec.Stage != pipeline.StageNormalize {
|
||||
t.Fatalf("NPC normalizer spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
||||
}
|
||||
if spec, ok := registries.Extractors.Spec(combatextract.Key); !ok || spec.ArtifactKind != dnd.CombatTurnListKind {
|
||||
t.Fatalf("combat extractor spec = %#v, present = %t; want dnd combat-turn-list artifact", spec, ok)
|
||||
}
|
||||
if spec, ok := registries.Normalizers.Spec(combatnormalize.Key); !ok || spec.ArtifactKind != dnd.CombatTurnListKind || spec.Stage != pipeline.StageNormalize {
|
||||
t.Fatalf("combat normalizer spec = %#v, present = %t; want dnd combat-turn-list artifact", spec, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendNPCListsPreservesOrderAndArrayPresence(t *testing.T) {
|
||||
@@ -143,6 +185,44 @@ func TestAppendNPCListsPreservesOrderAndArrayPresence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendCombatTurnListsPreservesOrderPresenceAndOwnership(t *testing.T) {
|
||||
round := 1
|
||||
resolution := "hit"
|
||||
targets := []string{"Mira"}
|
||||
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}
|
||||
input := []dnd.CombatTurnList{
|
||||
{CombatTurns: []dnd.CombatTurn{{Actor: "first", Round: &round, Actions: []dnd.CombatAction{{Targets: targets, Resolution: &resolution}}, SourceRefs: refs}}},
|
||||
{CombatTurns: []dnd.CombatTurn{{Actor: "second"}}},
|
||||
}
|
||||
got, err := appendCombatTurnLists(input)
|
||||
if err != nil {
|
||||
t.Fatalf("appendCombatTurnLists() error = %v, want nil", err)
|
||||
}
|
||||
if len(got.CombatTurns) != 2 || got.CombatTurns[0].Actor != "first" || got.CombatTurns[1].Actor != "second" {
|
||||
t.Fatalf("combat turns = %#v, want chunk order", got.CombatTurns)
|
||||
}
|
||||
if got.CombatTurns[0].Round == &round || &got.CombatTurns[0].Actions[0].Targets[0] == &targets[0] || got.CombatTurns[0].Actions[0].Resolution == &resolution || &got.CombatTurns[0].SourceRefs[0] == &refs[0] {
|
||||
t.Fatal("appendCombatTurnLists() retained nested input aliases")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
in []dnd.CombatTurnList
|
||||
want dnd.CombatTurnList
|
||||
}{
|
||||
{name: "no values", in: nil, want: dnd.CombatTurnList{}},
|
||||
{name: "nil values", in: []dnd.CombatTurnList{{}, {}}, want: dnd.CombatTurnList{}},
|
||||
{name: "present empty", in: []dnd.CombatTurnList{{CombatTurns: []dnd.CombatTurn{}}}, want: dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{}}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := appendCombatTurnLists(test.in)
|
||||
if err != nil || !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("appendCombatTurnLists() = %#v, error = %v, want %#v", got, err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterRejectsMissingDNDDependenciesBeforeMutation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user