Centralize D&D extraction request preparation

This commit is contained in:
2026-07-24 14:38:42 +00:00
parent 8e0b029f5f
commit e2cb0d901a
15 changed files with 144 additions and 185 deletions

View File

@@ -2,14 +2,30 @@ package shared
import (
"bytes"
"context"
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
// ChunkPromptMaterial prepares the chunk-scoped source material used by D&D
// extractors when constructing their prompt inputs.
func ChunkPromptMaterial(req contracts.TypedExtractionRequest) (contracts.LLMInputMaterial, error) {
// PrepareChunkExtraction validates common D&D extraction prerequisites and
// prepares owned chunk-scoped source material for prompt inputs.
func PrepareChunkExtraction(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.LLMInputMaterial, error) {
if ctx == nil {
return contracts.LLMInputMaterial{}, fmt.Errorf("context must not be nil")
}
if err := ctx.Err(); err != nil {
return contracts.LLMInputMaterial{}, fmt.Errorf("context error before extraction: %w", err)
}
if req.Source == nil {
return contracts.LLMInputMaterial{}, fmt.Errorf("source must not be nil")
}
if req.Chunk == nil {
return contracts.LLMInputMaterial{}, fmt.Errorf("chunk must not be nil")
}
if len(req.Chunk.Units) == 0 {
return contracts.LLMInputMaterial{}, fmt.Errorf("chunk %q units must not be empty", req.Chunk.ID)
}
material := req.SourceInput.Clone()
if len(material.Content) == 0 {
material = contracts.NewLLMInputMaterial("source", req.Chunk.MediaType, req.Chunk.Content, "", "")

View File

@@ -1,6 +1,7 @@
package shared
import (
"context"
"reflect"
"strings"
"testing"
@@ -9,48 +10,78 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
func TestChunkPromptMaterial(t *testing.T) {
chunk := &source.Chunk{
ID: "session-alpha:chunk:0",
Content: []byte(`{"units":[1,2]}`),
MediaType: "application/json",
}
func TestPrepareChunkExtraction(t *testing.T) {
canceled, cancel := context.WithCancel(context.Background())
cancel()
tests := []struct {
name string
ctx context.Context
configure func(*contracts.TypedExtractionRequest)
sourceInput contracts.LLMInputMaterial
want contracts.LLMInputMaterial
wantErr string
mutateOutput bool
}{
{
name: "fallback to chunk content",
want: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "", ""),
name: "nil context",
wantErr: "context must not be nil",
},
{
name: "canceled context",
ctx: canceled,
wantErr: "context error before extraction",
},
{
name: "nil source",
configure: func(req *contracts.TypedExtractionRequest) {
req.Source = nil
},
wantErr: "source must not be nil",
},
{
name: "nil chunk",
configure: func(req *contracts.TypedExtractionRequest) {
req.Chunk = nil
},
wantErr: "chunk must not be nil",
},
{
name: "empty chunk units",
configure: func(req *contracts.TypedExtractionRequest) {
req.Chunk = &source.Chunk{ID: req.Chunk.ID}
},
wantErr: "units must not be empty",
},
{
name: "fallback to chunk content",
want: contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"units":[1,2]}`), "", ""),
mutateOutput: true,
},
{
name: "clone isolation",
sourceInput: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "sha256:source", "file:///source.json"),
want: contracts.NewLLMInputMaterial("source", chunk.MediaType, chunk.Content, "sha256:source", "file:///source.json"),
sourceInput: contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"units":[1,2]}`), "sha256:source", "file:///source.json"),
want: contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"units":[1,2]}`), "sha256:source", "file:///source.json"),
mutateOutput: true,
},
{
name: "mismatched content",
sourceInput: contracts.NewLLMInputMaterial("source", chunk.MediaType, []byte(`{"units":[9]}`), "sha256:other", "file:///other.json"),
sourceInput: contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"units":[9]}`), "sha256:other", "file:///other.json"),
wantErr: "source input must match chunk",
},
{
name: "default fields",
sourceInput: contracts.LLMInputMaterial{
Content: append([]byte(nil), chunk.Content...),
Content: []byte(`{"units":[1,2]}`),
Digest: "sha256:source",
OriginURI: "file:///source.json",
},
want: contracts.LLMInputMaterial{
Name: "source",
MediaType: chunk.MediaType,
Content: append([]byte(nil), chunk.Content...),
MediaType: "application/json",
Content: []byte(`{"units":[1,2]}`),
Digest: "sha256:source",
OriginURI: "file:///source.json",
SizeBytes: int64(len(chunk.Content)),
SizeBytes: int64(len(`{"units":[1,2]}`)),
},
},
{
@@ -58,7 +89,7 @@ func TestChunkPromptMaterial(t *testing.T) {
sourceInput: contracts.LLMInputMaterial{
Name: "transcript",
MediaType: "text/plain",
Content: append([]byte(nil), chunk.Content...),
Content: []byte(`{"units":[1,2]}`),
Digest: "sha256:explicit",
OriginURI: "file:///explicit.txt",
SizeBytes: 42,
@@ -66,7 +97,7 @@ func TestChunkPromptMaterial(t *testing.T) {
want: contracts.LLMInputMaterial{
Name: "transcript",
MediaType: "text/plain",
Content: append([]byte(nil), chunk.Content...),
Content: []byte(`{"units":[1,2]}`),
Digest: "sha256:explicit",
OriginURI: "file:///explicit.txt",
SizeBytes: 42,
@@ -76,26 +107,46 @@ func TestChunkPromptMaterial(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := contracts.TypedExtractionRequest{Chunk: chunk, SourceInput: test.sourceInput}
got, err := ChunkPromptMaterial(req)
req := extractionRequest()
req.SourceInput = test.sourceInput
if test.configure != nil {
test.configure(&req)
}
ctx := test.ctx
if ctx == nil && test.wantErr != "context must not be nil" {
ctx = context.Background()
}
got, err := PrepareChunkExtraction(ctx, req)
if test.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("ChunkPromptMaterial() error = %v, want %q", err, test.wantErr)
t.Fatalf("PrepareChunkExtraction() error = %v, want %q", err, test.wantErr)
}
return
}
if err != nil {
t.Fatalf("ChunkPromptMaterial() error = %v, want nil", err)
t.Fatalf("PrepareChunkExtraction() error = %v, want nil", err)
}
if !reflect.DeepEqual(got, test.want) {
t.Fatalf("ChunkPromptMaterial() = %#v, want %#v", got, test.want)
t.Fatalf("PrepareChunkExtraction() = %#v, want %#v", got, test.want)
}
if test.mutateOutput {
got.Content[0] = 'x'
if string(test.sourceInput.Content) != string(chunk.Content) {
t.Fatalf("ChunkPromptMaterial() output shares content with source input")
if string(req.SourceInput.Content) != string(test.sourceInput.Content) || string(req.Chunk.Content) != `{"units":[1,2]}` {
t.Fatal("PrepareChunkExtraction() output shares request content")
}
}
})
}
}
func extractionRequest() contracts.TypedExtractionRequest {
doc := &source.SourceDocument{ID: "session-alpha"}
chunk := &source.Chunk{
ID: "session-alpha:chunk:0",
SourceID: doc.ID,
Content: []byte(`{"units":[1,2]}`),
MediaType: "application/json",
Units: []source.SourceUnit{{ID: 1}},
}
return contracts.TypedExtractionRequest{Source: doc, Chunk: chunk}
}