Enable correction-aware foundational D&D producers
This commit is contained in:
@@ -42,6 +42,13 @@ unknown fields, while preserving semantic candidates for deterministic
|
|||||||
validation. Do not promote a private response envelope into a durable schema;
|
validation. Do not promote a private response envelope into a durable schema;
|
||||||
the contracts above define durable data.
|
the contracts above define durable data.
|
||||||
|
|
||||||
|
A D&D producer that declares `single_response_v1` forwards any supplied
|
||||||
|
semantic correction to its structured completion and returns an owned copy of
|
||||||
|
that completion's exact validated raw response as its model candidate. It does
|
||||||
|
not serialize normalized artifacts to create that candidate, so deterministic
|
||||||
|
identity, evidence, warning, and durable-schema behavior remains separate from
|
||||||
|
the model transport material.
|
||||||
|
|
||||||
## Prompt Construction
|
## Prompt Construction
|
||||||
|
|
||||||
D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a
|
D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ changing ordinary requests.
|
|||||||
Notarius has one safe, tested adapter path for fresh corrected requests, and no
|
Notarius has one safe, tested adapter path for fresh corrected requests, and no
|
||||||
stage invokes it yet. This stage is one Terra prompt.
|
stage invokes it yet. This stage is one Terra prompt.
|
||||||
|
|
||||||
## Stage 6 — Migrate The Scene Chunker And Foundational Extractors
|
## Stage 6 — Migrate The Scene Chunker And Foundational Extractors ✅
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -94,33 +94,40 @@ func (c *Chunker) Plan(ctx context.Context, req contracts.ChunkRequest) (contrac
|
|||||||
return contracts.ChunkPlanResult{}, chunkerErrorf("validate source document: %w", err)
|
return contracts.ChunkPlanResult{}, chunkerErrorf("validate source document: %w", err)
|
||||||
}
|
}
|
||||||
var response chunkResponse
|
var response chunkResponse
|
||||||
if _, err := c.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
completion, err := c.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||||
StageName: Key,
|
StageName: Key,
|
||||||
PromptID: PromptID,
|
PromptID: PromptID,
|
||||||
PromptVersion: ResponseSchemaVersion,
|
PromptVersion: ResponseSchemaVersion,
|
||||||
ProfileID: req.LLMProfile,
|
ProfileID: req.LLMProfile,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
||||||
|
Correction: req.Correction,
|
||||||
Inputs: shared.PromptInputs(req.SourceInput, req.References),
|
Inputs: shared.PromptInputs(req.SourceInput, req.References),
|
||||||
}, &response); err != nil {
|
}, &response)
|
||||||
|
if err != nil {
|
||||||
return contracts.ChunkPlanResult{}, chunkerErrorf("complete structured output: %w", err)
|
return contracts.ChunkPlanResult{}, chunkerErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
|
candidate, err := shared.ModelCandidateFromResponse(completion)
|
||||||
|
if err != nil {
|
||||||
|
return contracts.ChunkPlanResult{}, chunkerErrorf("capture model candidate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
plan, err := planFromResponse(req.Source, response)
|
plan, err := planFromResponse(req.Source, response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.ChunkPlanResult{}, chunkerErrorf("malformed structured output: %w", err)
|
return contracts.ChunkPlanResult{}, chunkerErrorf("malformed structured output: %w", err)
|
||||||
}
|
}
|
||||||
return contracts.ChunkPlanResult{Plan: plan}, nil
|
return contracts.ChunkPlanResult{Plan: plan, ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{
|
return pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageChunk,
|
Stage: pipeline.StageChunk,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions),
|
Provides: append([]string(nil), providedCapabilities...),
|
||||||
|
ReferenceSlots: shared.ReferenceSlots(referenceSlotDescriptions),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ func TestNewModuleSpecAndRegister(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := pipeline.ModuleSpec{
|
want := pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageChunk,
|
Stage: pipeline.StageChunk,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: []string{"source.transcript"},
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Provides: []string{"chunks"},
|
Requires: []string{"source.transcript"},
|
||||||
ReferenceSlots: wantReferenceSlots(),
|
Provides: []string{"chunks"},
|
||||||
|
ReferenceSlots: wantReferenceSlots(),
|
||||||
}
|
}
|
||||||
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
||||||
@@ -181,6 +182,13 @@ func TestPlanReturnsAnnotationFreeSceneRangesFromStructuredOutput(t *testing.T)
|
|||||||
if len(result.Warnings) != 0 {
|
if len(result.Warnings) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want absent", result.Warnings)
|
t.Fatalf("warnings = %#v, want absent", result.Warnings)
|
||||||
}
|
}
|
||||||
|
wantCandidate, err := json.Marshal(client.response)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal expected candidate: %v", err)
|
||||||
|
}
|
||||||
|
if result.ModelCandidate == nil || result.ModelCandidate.Protocol != contracts.CorrectionProtocolSingleResponseV1 || !reflect.DeepEqual(result.ModelCandidate.Response, wantCandidate) {
|
||||||
|
t.Fatalf("model candidate = %#v, want exact validated response", result.ModelCandidate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPlanUsesDocumentOrderForNonconsecutiveUnitIDs(t *testing.T) {
|
func TestPlanUsesDocumentOrderForNonconsecutiveUnitIDs(t *testing.T) {
|
||||||
|
|||||||
@@ -109,21 +109,28 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
order := shared.NewSourceRefOrder(req.Source)
|
order := shared.NewSourceRefOrder(req.Source)
|
||||||
|
|
||||||
var response extractionResponse
|
var response extractionResponse
|
||||||
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||||
StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion,
|
StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion,
|
||||||
ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
||||||
Inputs: shared.PromptInputs(sourceInput, req.References),
|
Correction: req.Correction,
|
||||||
}, &response); err != nil {
|
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||||
|
}, &response)
|
||||||
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.ItemRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.ItemRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
|
candidate, err := shared.ModelCandidateFromResponse(completion)
|
||||||
|
if err != nil {
|
||||||
|
return contracts.TypedExtractionResult[dnd.ItemRegistry]{}, extractorErrorf("capture model candidate: %w", err)
|
||||||
|
}
|
||||||
canonicalizeResponse(&response, order, req.Source.ID)
|
canonicalizeResponse(&response, order, req.Source.ID)
|
||||||
return contracts.TypedExtractionResult[dnd.ItemRegistry]{Value: canonicalItemRegistry(response, req.Source.ID)}, nil
|
return contracts.TypedExtractionResult[dnd.ItemRegistry]{Value: canonicalItemRegistry(response, req.Source.ID), ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{
|
return pipeline.ModuleSpec{
|
||||||
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked,
|
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...),
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
|
Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...),
|
||||||
ArtifactKind: dnd.ItemRegistryKind, ReferenceSlots: referenceSlots(),
|
ArtifactKind: dnd.ItemRegistryKind, ReferenceSlots: referenceSlots(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestModuleRegistrationMetadataAndRedaction(t *testing.T) {
|
|||||||
if _, err := New(&fakeItemsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one") {
|
if _, err := New(&fakeItemsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one") {
|
||||||
t.Fatalf("New() error = %v, want reference-set rejection", err)
|
t.Fatalf("New() error = %v, want reference-set rejection", err)
|
||||||
}
|
}
|
||||||
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.item_registry"}, ArtifactKind: dnd.ItemRegistryKind, ReferenceSlots: referenceSlots()}
|
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.item_registry"}, ArtifactKind: dnd.ItemRegistryKind, ReferenceSlots: referenceSlots()}
|
||||||
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,21 +109,28 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
order := shared.NewSourceRefOrder(req.Source)
|
order := shared.NewSourceRefOrder(req.Source)
|
||||||
|
|
||||||
var response extractionResponse
|
var response extractionResponse
|
||||||
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||||
StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion,
|
StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion,
|
||||||
ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
ProfileID: req.LLMProfile, SessionID: req.SessionID, StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
||||||
Inputs: shared.PromptInputs(sourceInput, req.References),
|
Correction: req.Correction,
|
||||||
}, &response); err != nil {
|
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||||
|
}, &response)
|
||||||
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
|
candidate, err := shared.ModelCandidateFromResponse(completion)
|
||||||
|
if err != nil {
|
||||||
|
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("capture model candidate: %w", err)
|
||||||
|
}
|
||||||
canonicalizeResponse(&response, order, req.Source.ID)
|
canonicalizeResponse(&response, order, req.Source.ID)
|
||||||
return contracts.TypedExtractionResult[dnd.LocationRegistry]{Value: canonicalLocationRegistry(response, req.Source.ID)}, nil
|
return contracts.TypedExtractionResult[dnd.LocationRegistry]{Value: canonicalLocationRegistry(response, req.Source.ID), ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{
|
return pipeline.ModuleSpec{
|
||||||
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked,
|
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...),
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
|
Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...),
|
||||||
ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots(),
|
ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestModuleRegistrationAndMetadata(t *testing.T) {
|
|||||||
if _, err := New(&fakeLocationsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one") {
|
if _, err := New(&fakeLocationsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one") {
|
||||||
t.Fatalf("New() error = %v, want reference-set rejection", err)
|
t.Fatalf("New() error = %v, want reference-set rejection", err)
|
||||||
}
|
}
|
||||||
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.location_registry"}, ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots()}
|
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.location_registry"}, ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots()}
|
||||||
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,30 +117,37 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
order := shared.NewSourceRefOrder(req.Source)
|
order := shared.NewSourceRefOrder(req.Source)
|
||||||
|
|
||||||
var response extractionResponse
|
var response extractionResponse
|
||||||
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||||
StageName: Key,
|
StageName: Key,
|
||||||
PromptID: PromptID,
|
PromptID: PromptID,
|
||||||
PromptVersion: SchemaVersion,
|
PromptVersion: SchemaVersion,
|
||||||
ProfileID: req.LLMProfile,
|
ProfileID: req.LLMProfile,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
||||||
|
Correction: req.Correction,
|
||||||
Inputs: shared.PromptInputs(sourceInput, req.References),
|
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||||
}, &response); err != nil {
|
}, &response)
|
||||||
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
|
candidate, err := shared.ModelCandidateFromResponse(completion)
|
||||||
|
if err != nil {
|
||||||
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("capture model candidate: %w", err)
|
||||||
|
}
|
||||||
canonicalizeResponse(&response, order, req.Source.ID)
|
canonicalizeResponse(&response, order, req.Source.ID)
|
||||||
return contracts.TypedExtractionResult[dnd.NPCRegistry]{Value: canonicalNPCRegistry(response, req.Source.ID)}, nil
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{Value: canonicalNPCRegistry(response, req.Source.ID), ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{
|
return pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageExtract,
|
Stage: pipeline.StageExtract,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
ArtifactKind: dnd.NPCRegistryKind,
|
Provides: append([]string(nil), providedCapabilities...),
|
||||||
ReferenceSlots: referenceSlots(),
|
ArtifactKind: dnd.NPCRegistryKind,
|
||||||
|
ReferenceSlots: referenceSlots(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,38 @@ func TestExtractMapsRawSemanticCandidatesWithoutRepair(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractForwardsCorrectionAndOwnsModelCandidate(t *testing.T) {
|
||||||
|
rawResponse := []byte(`{"npcs":[{"name":"Mira Thorn","source_refs":[{"start_unit_id":1,"end_unit_id":1}]}]}`)
|
||||||
|
client := &fakeNPCsLLMClient{content: append([]byte(nil), rawResponse...)}
|
||||||
|
correction, err := contracts.NewSemanticCorrection([]byte(`{"npcs":[]}`), "Retain the transcript-grounded NPC.")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSemanticCorrection() error = %v", err)
|
||||||
|
}
|
||||||
|
request := extractionRequest()
|
||||||
|
request.Correction = correction
|
||||||
|
result, err := newExtractor(t, client).Extract(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Extract() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if len(client.requests) != 1 || client.requests[0].Correction == nil ||
|
||||||
|
string(client.requests[0].Correction.AssistantResponse) != `{"npcs":[]}` ||
|
||||||
|
client.requests[0].Correction.UserGuidance != "Retain the transcript-grounded NPC." {
|
||||||
|
t.Fatalf("structured request correction = %#v, want forwarded correction", client.requests)
|
||||||
|
}
|
||||||
|
correction.AssistantResponse[0] = '['
|
||||||
|
if got := string(client.requests[0].Correction.AssistantResponse); got != `{"npcs":[]}` {
|
||||||
|
t.Fatalf("captured correction changed after caller mutation: %q", got)
|
||||||
|
}
|
||||||
|
if result.ModelCandidate == nil || result.ModelCandidate.Protocol != contracts.CorrectionProtocolSingleResponseV1 ||
|
||||||
|
string(result.ModelCandidate.Response) != string(rawResponse) {
|
||||||
|
t.Fatalf("model candidate = %#v, want exact validated response", result.ModelCandidate)
|
||||||
|
}
|
||||||
|
client.content[0] = '['
|
||||||
|
if got := string(result.ModelCandidate.Response); got != string(rawResponse) {
|
||||||
|
t.Fatalf("model candidate changed after provider buffer mutation: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractRetainsLocalErrorContextAndProviderFailures(t *testing.T) {
|
func TestExtractRetainsLocalErrorContextAndProviderFailures(t *testing.T) {
|
||||||
request := extractionRequest()
|
request := extractionRequest()
|
||||||
extractor := newExtractor(t, &fakeNPCsLLMClient{response: extractionResponse{NPCs: []npcResponse{}}})
|
extractor := newExtractor(t, &fakeNPCsLLMClient{response: extractionResponse{NPCs: []npcResponse{}}})
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ func TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) {
|
|||||||
func TestModuleSpecAndReferenceSlots(t *testing.T) {
|
func TestModuleSpecAndReferenceSlots(t *testing.T) {
|
||||||
got := ModuleSpec()
|
got := ModuleSpec()
|
||||||
want := pipeline.ModuleSpec{
|
want := pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageExtract,
|
Stage: pipeline.StageExtract,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: []string{"chunks", "source.transcript"},
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Provides: []string{"dnd.npc_registry"},
|
Requires: []string{"chunks", "source.transcript"},
|
||||||
ArtifactKind: dnd.NPCRegistryKind,
|
Provides: []string{"dnd.npc_registry"},
|
||||||
|
ArtifactKind: dnd.NPCRegistryKind,
|
||||||
ReferenceSlots: []contracts.ReferenceSlot{
|
ReferenceSlots: []contracts.ReferenceSlot{
|
||||||
{Name: "glossary", Description: "Optional campaign glossary reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
{Name: "glossary", Description: "Optional campaign glossary reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||||
{Name: "party", Description: "Optional party roster reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
{Name: "party", Description: "Optional party roster reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||||
|
|||||||
@@ -119,18 +119,24 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var response extractionResponse
|
var response extractionResponse
|
||||||
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||||
StageName: Key,
|
StageName: Key,
|
||||||
PromptID: PromptID,
|
PromptID: PromptID,
|
||||||
PromptVersion: SchemaVersion,
|
PromptVersion: SchemaVersion,
|
||||||
ProfileID: req.LLMProfile,
|
ProfileID: req.LLMProfile,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
||||||
|
Correction: req.Correction,
|
||||||
Inputs: shared.PromptInputs(sourceInput, req.References),
|
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||||
}, &response); err != nil {
|
}, &response)
|
||||||
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.SceneDescriptionList]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.SceneDescriptionList]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
return contracts.TypedExtractionResult[dnd.SceneDescriptionList]{Value: mapResponse(response, req.Chunk)}, nil
|
candidate, err := shared.ModelCandidateFromResponse(completion)
|
||||||
|
if err != nil {
|
||||||
|
return contracts.TypedExtractionResult[dnd.SceneDescriptionList]{}, extractorErrorf("capture model candidate: %w", err)
|
||||||
|
}
|
||||||
|
return contracts.TypedExtractionResult[dnd.SceneDescriptionList]{Value: mapResponse(response, req.Chunk), ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapResponse(response extractionResponse, chunk *source.Chunk) dnd.SceneDescriptionList {
|
func mapResponse(response extractionResponse, chunk *source.Chunk) dnd.SceneDescriptionList {
|
||||||
@@ -145,13 +151,14 @@ func mapResponse(response extractionResponse, chunk *source.Chunk) dnd.SceneDesc
|
|||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{
|
return pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageExtract,
|
Stage: pipeline.StageExtract,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
ArtifactKind: dnd.SceneDescriptionListKind,
|
Provides: append([]string(nil), providedCapabilities...),
|
||||||
ReferenceSlots: referenceSlots(),
|
ArtifactKind: dnd.SceneDescriptionListKind,
|
||||||
|
ReferenceSlots: referenceSlots(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) {
|
|||||||
|
|
||||||
func TestModuleSpecAndReferenceSlots(t *testing.T) {
|
func TestModuleSpecAndReferenceSlots(t *testing.T) {
|
||||||
want := pipeline.ModuleSpec{
|
want := pipeline.ModuleSpec{
|
||||||
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.scene_descriptions"}, ArtifactKind: dnd.SceneDescriptionListKind,
|
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.scene_descriptions"}, ArtifactKind: dnd.SceneDescriptionListKind,
|
||||||
ReferenceSlots: []contracts.ReferenceSlot{
|
ReferenceSlots: []contracts.ReferenceSlot{
|
||||||
{Name: "glossary", Description: "Optional campaign glossary reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
{Name: "glossary", Description: "Optional campaign glossary reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||||
{Name: "party", Description: "Optional party roster reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
{Name: "party", Description: "Optional party roster reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||||
|
|||||||
@@ -182,30 +182,37 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
inputs := shared.PromptInputs(sourceInput, req.References)
|
inputs := shared.PromptInputs(sourceInput, req.References)
|
||||||
inputs[spellcatalog.SpellCatalogReferenceSlot] = e.catalogPromptInput.Clone()
|
inputs[spellcatalog.SpellCatalogReferenceSlot] = e.catalogPromptInput.Clone()
|
||||||
inputs[NPCRegistryReferenceSlot] = npcRegistry.PromptInput()
|
inputs[NPCRegistryReferenceSlot] = npcRegistry.PromptInput()
|
||||||
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
completion, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||||
StageName: Key,
|
StageName: Key,
|
||||||
PromptID: PromptID,
|
PromptID: PromptID,
|
||||||
PromptVersion: SchemaVersion,
|
PromptVersion: SchemaVersion,
|
||||||
ProfileID: req.LLMProfile,
|
ProfileID: req.LLMProfile,
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
StructuredOutputRepairAttempts: req.StructuredOutputRepairAttempts,
|
||||||
|
Correction: req.Correction,
|
||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
}, &response); err != nil {
|
}, &response)
|
||||||
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.SpellList]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.SpellList]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
|
candidate, err := shared.ModelCandidateFromResponse(completion)
|
||||||
|
if err != nil {
|
||||||
|
return contracts.TypedExtractionResult[dnd.SpellList]{}, extractorErrorf("capture model candidate: %w", err)
|
||||||
|
}
|
||||||
canonicalizeResponse(&response, order, req.Source.ID)
|
canonicalizeResponse(&response, order, req.Source.ID)
|
||||||
return contracts.TypedExtractionResult[dnd.SpellList]{Value: canonicalSpellList(response, req.Source.ID)}, nil
|
return contracts.TypedExtractionResult[dnd.SpellList]{Value: canonicalSpellList(response, req.Source.ID), ModelCandidate: candidate}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{
|
return pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageExtract,
|
Stage: pipeline.StageExtract,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
ArtifactKind: dnd.SpellListKind,
|
Provides: append([]string(nil), providedCapabilities...),
|
||||||
ReferenceSlots: referenceSlots(),
|
ArtifactKind: dnd.SpellListKind,
|
||||||
|
ReferenceSlots: referenceSlots(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ func TestNewRequiresLLMClientAndReturnsExtractor(t *testing.T) {
|
|||||||
func TestModuleSpec(t *testing.T) {
|
func TestModuleSpec(t *testing.T) {
|
||||||
got := ModuleSpec()
|
got := ModuleSpec()
|
||||||
want := pipeline.ModuleSpec{
|
want := pipeline.ModuleSpec{
|
||||||
Key: Key,
|
Key: Key,
|
||||||
Stage: pipeline.StageExtract,
|
Stage: pipeline.StageExtract,
|
||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
|
CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
|
||||||
Requires: []string{
|
Requires: []string{
|
||||||
"chunks",
|
"chunks",
|
||||||
"source.transcript",
|
"source.transcript",
|
||||||
|
|||||||
9
internal/modules/dnd/shared/model_candidate.go
Normal file
9
internal/modules/dnd/shared/model_candidate.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
|
|
||||||
|
// ModelCandidateFromResponse returns an owned candidate for the exact validated
|
||||||
|
// structured response produced by the shared LLM boundary.
|
||||||
|
func ModelCandidateFromResponse(response contracts.StructuredCompletionResponse) (*contracts.ModelCandidate, error) {
|
||||||
|
return contracts.NewModelCandidate(response.Content, contracts.CorrectionProtocolSingleResponseV1)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user