Relax private D&D response schemas
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
dndregister "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/register"
|
||||
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"
|
||||
genericregister "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/register"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
||||
seriatimregister "gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/register"
|
||||
@@ -131,7 +132,7 @@ func TestRunnerProcessesSeriatimInputWithProductionDNDNPCPipeline(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionNPCPipelineAttributesInvalidShapeBeforeSchemaValidation(t *testing.T) {
|
||||
func TestProductionNPCPipelineRoutesSemanticCandidatesToDeterministicValidators(t *testing.T) {
|
||||
registries := productionNPCRegistries(t)
|
||||
effective, err := loadNPCPipelineConfig(t).Resolve(config.ResolveInput{
|
||||
PipelineID: "dnd-npcs-fixture",
|
||||
@@ -140,23 +141,51 @@ func TestProductionNPCPipelineAttributesInvalidShapeBeforeSchemaValidation(t *te
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v, want nil", err)
|
||||
}
|
||||
client := &fakeNPCProductionLLMClient{response: npcProductionResponse{NPCs: []npcProductionRecord{{
|
||||
Name: "",
|
||||
Aliases: []string{},
|
||||
Description: "A participant.",
|
||||
Relationships: []npcProductionRelationship{},
|
||||
SourceRefs: []npcProductionSourceRef{{StartUnitID: 1, EndUnitID: 1}},
|
||||
}}}}
|
||||
output, err := runPreparedPipeline(t, registries, effective.ResolvedPipeline, client, pipeline.RunInput{RawInput: readNPCFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v, want non-fatal rejected output", err)
|
||||
}
|
||||
if len(client.requests) != 3 || len(output.Rejected) != 1 || len(output.NormalizeOutputs) != 0 {
|
||||
t.Fatalf("LLM requests = %d rejected = %#v normalized = %#v, want exhausted shape rejection", len(client.requests), output.Rejected, output.NormalizeOutputs)
|
||||
}
|
||||
rejection := output.Rejected[0]
|
||||
if rejection.ReasonCode != npcshape.ReasonCode || rejection.ValidatorName != npcshape.Key || rejection.AttemptCount != 3 {
|
||||
t.Fatalf("rejection = %#v, want exhausted NPC shape rejection", rejection)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
response []byte
|
||||
reasonCode string
|
||||
validatorName string
|
||||
}{
|
||||
{
|
||||
name: "blank string",
|
||||
response: []byte(`{"npcs":[{"name":"","aliases":[],"description":"A participant.","relationships":[],"source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`),
|
||||
reasonCode: npcshape.ReasonCode,
|
||||
validatorName: npcshape.Key,
|
||||
},
|
||||
{
|
||||
name: "empty evidence",
|
||||
response: []byte(`{"npcs":[{"name":"Mira Thorn","aliases":[],"description":"A participant.","relationships":[],"source_refs":[]}]}`),
|
||||
reasonCode: npcshape.ReasonCode,
|
||||
validatorName: npcshape.Key,
|
||||
},
|
||||
{
|
||||
name: "nonpositive unit candidate",
|
||||
response: []byte(`{"npcs":[{"name":"Mira Thorn","aliases":[],"description":"A participant.","relationships":[],"source_refs":[{"start_unit_id":0,"end_unit_id":1}]}]}`),
|
||||
reasonCode: npcsourcerefs.ReasonCode,
|
||||
validatorName: npcsourcerefs.Key,
|
||||
},
|
||||
{
|
||||
name: "unknown unit candidate",
|
||||
response: []byte(`{"npcs":[{"name":"Mira Thorn","aliases":[],"description":"A participant.","relationships":[],"source_refs":[{"start_unit_id":99,"end_unit_id":99}]}]}`),
|
||||
reasonCode: npcsourcerefs.ReasonCode,
|
||||
validatorName: npcsourcerefs.Key,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := &fakeNPCProductionLLMClient{rawResponses: [][]byte{test.response, test.response, test.response}}
|
||||
output, err := runPreparedPipeline(t, registries, effective.ResolvedPipeline, client, pipeline.RunInput{RawInput: readNPCFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v, want non-fatal rejected output", err)
|
||||
}
|
||||
if len(client.requests) != 3 || len(output.Rejected) != 1 || len(output.NormalizeOutputs) != 0 {
|
||||
t.Fatalf("LLM requests = %d rejected = %#v normalized = %#v, want exhausted rejection", len(client.requests), output.Rejected, output.NormalizeOutputs)
|
||||
}
|
||||
rejection := output.Rejected[0]
|
||||
if rejection.ReasonCode != test.reasonCode || rejection.ValidatorName != test.validatorName || rejection.AttemptCount != 3 {
|
||||
t.Fatalf("rejection = %#v, want exhausted %s rejection", rejection, test.validatorName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,15 +212,26 @@ type npcProductionSourceRef struct {
|
||||
}
|
||||
|
||||
type fakeNPCProductionLLMClient struct {
|
||||
response npcProductionResponse
|
||||
requests []contracts.StructuredCompletionRequest
|
||||
response npcProductionResponse
|
||||
rawResponses [][]byte
|
||||
requests []contracts.StructuredCompletionRequest
|
||||
}
|
||||
|
||||
func (client *fakeNPCProductionLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
client.requests = append(client.requests, req)
|
||||
content, err := json.Marshal(client.response)
|
||||
if err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, err
|
||||
var content []byte
|
||||
if client.rawResponses != nil {
|
||||
index := len(client.requests) - 1
|
||||
if index >= len(client.rawResponses) {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("missing fake NPC response %d", index)
|
||||
}
|
||||
content = append([]byte(nil), client.rawResponses[index]...)
|
||||
} else {
|
||||
var err error
|
||||
content, err = json.Marshal(client.response)
|
||||
if err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, err
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(content, out); err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("populate NPC structured target: %w", err)
|
||||
|
||||
@@ -22,24 +22,35 @@ type spellCastResponse struct {
|
||||
}
|
||||
|
||||
type fakeSpellsLLMClient struct {
|
||||
response extractionResponse
|
||||
responses []extractionResponse
|
||||
requests []contracts.StructuredCompletionRequest
|
||||
response extractionResponse
|
||||
responses []extractionResponse
|
||||
rawResponses [][]byte
|
||||
requests []contracts.StructuredCompletionRequest
|
||||
}
|
||||
|
||||
func (client *fakeSpellsLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
client.requests = append(client.requests, cloneStructuredCompletionRequest(req))
|
||||
response := client.response
|
||||
if client.responses != nil {
|
||||
var content []byte
|
||||
if client.rawResponses != nil {
|
||||
index := len(client.requests) - 1
|
||||
if index >= len(client.responses) {
|
||||
if index >= len(client.rawResponses) {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("missing fake response %d", index)
|
||||
}
|
||||
response = client.responses[index]
|
||||
}
|
||||
content, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, err
|
||||
content = append([]byte(nil), client.rawResponses[index]...)
|
||||
} else {
|
||||
response := client.response
|
||||
if client.responses != nil {
|
||||
index := len(client.requests) - 1
|
||||
if index >= len(client.responses) {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("missing fake response %d", index)
|
||||
}
|
||||
response = client.responses[index]
|
||||
}
|
||||
var err error
|
||||
content, err = json.Marshal(response)
|
||||
if err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, err
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(content, out); err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("populate structured target: %w", err)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||
spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells"
|
||||
spellshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/shape"
|
||||
spellsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/source_refs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
||||
)
|
||||
|
||||
@@ -166,7 +167,7 @@ func TestRunnerPassesPartyAndGlossaryReferencesToDNDSpellsPrompt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionSpellPipelineAttributesInvalidShapeBeforeSchemaValidation(t *testing.T) {
|
||||
func TestProductionSpellPipelineRoutesSemanticCandidatesToDeterministicValidators(t *testing.T) {
|
||||
registries := productionNPCRegistries(t)
|
||||
configValue := config.Default()
|
||||
configValue.Pipelines["dnd-spells-shape"] = pipeline.PipelineProfile{
|
||||
@@ -186,24 +187,51 @@ func TestProductionSpellPipelineAttributesInvalidShapeBeforeSchemaValidation(t *
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v, want nil", err)
|
||||
}
|
||||
response := extractionResponse{SpellCasts: []spellCastResponse{{
|
||||
Caster: "Aria",
|
||||
Spell: "Cure Wounds",
|
||||
Effect: "",
|
||||
NarrativeDescription: "Aria casts the spell.",
|
||||
SourceRefs: responseSourceRefs("spell-session", 1, 1),
|
||||
}}}
|
||||
client := &fakeSpellsLLMClient{responses: []extractionResponse{response, response, response}}
|
||||
output, err := runPreparedPipeline(t, registries, effective.ResolvedPipeline, client, pipeline.RunInput{RawInput: readDNDSpellsFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v, want non-fatal rejected output", err)
|
||||
}
|
||||
if len(client.requests) != 3 || len(output.Rejected) != 1 || len(output.NormalizeOutputs) != 0 {
|
||||
t.Fatalf("LLM requests = %d rejected = %#v normalized = %#v, want exhausted shape rejection", len(client.requests), output.Rejected, output.NormalizeOutputs)
|
||||
}
|
||||
rejection := output.Rejected[0]
|
||||
if rejection.ReasonCode != spellshape.ReasonCode || rejection.ValidatorName != spellshape.Key || rejection.AttemptCount != 3 {
|
||||
t.Fatalf("rejection = %#v, want exhausted spell shape rejection", rejection)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
response []byte
|
||||
reasonCode string
|
||||
validatorName string
|
||||
}{
|
||||
{
|
||||
name: "blank string",
|
||||
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"","narrative_description":"Aria casts the spell.","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`),
|
||||
reasonCode: spellshape.ReasonCode,
|
||||
validatorName: spellshape.Key,
|
||||
},
|
||||
{
|
||||
name: "empty evidence",
|
||||
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals.","narrative_description":"Aria casts the spell.","source_refs":[]}]}`),
|
||||
reasonCode: spellshape.ReasonCode,
|
||||
validatorName: spellshape.Key,
|
||||
},
|
||||
{
|
||||
name: "nonpositive unit candidate",
|
||||
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals.","narrative_description":"Aria casts the spell.","source_refs":[{"start_unit_id":0,"end_unit_id":1}]}]}`),
|
||||
reasonCode: spellsourcerefs.ReasonCode,
|
||||
validatorName: spellsourcerefs.Key,
|
||||
},
|
||||
{
|
||||
name: "unknown unit candidate",
|
||||
response: []byte(`{"spell_casts":[{"caster":"Aria","spell":"Cure Wounds","effect":"Heals.","narrative_description":"Aria casts the spell.","source_refs":[{"start_unit_id":99,"end_unit_id":99}]}]}`),
|
||||
reasonCode: spellsourcerefs.ReasonCode,
|
||||
validatorName: spellsourcerefs.Key,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := &fakeSpellsLLMClient{rawResponses: [][]byte{test.response, test.response, test.response}}
|
||||
output, err := runPreparedPipeline(t, registries, effective.ResolvedPipeline, client, pipeline.RunInput{RawInput: readDNDSpellsFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v, want non-fatal rejected output", err)
|
||||
}
|
||||
if len(client.requests) != 3 || len(output.Rejected) != 1 || len(output.NormalizeOutputs) != 0 {
|
||||
t.Fatalf("LLM requests = %d rejected = %#v normalized = %#v, want exhausted rejection", len(client.requests), output.Rejected, output.NormalizeOutputs)
|
||||
}
|
||||
rejection := output.Rejected[0]
|
||||
if rejection.ReasonCode != test.reasonCode || rejection.ValidatorName != test.validatorName || rejection.AttemptCount != 3 {
|
||||
t.Fatalf("rejection = %#v, want exhausted %s rejection", rejection, test.validatorName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user