Move D&D spell assets into extractor module
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -17,13 +18,21 @@ var schemaAssets embed.FS
|
||||
type ResponseSchemaKey string
|
||||
|
||||
const (
|
||||
DNDSpellsSchemaKey ResponseSchemaKey = "dnd_spells"
|
||||
TestArtifactSchemaKey ResponseSchemaKey = "test_artifact"
|
||||
TestValidatorDecisionSchemaKey ResponseSchemaKey = "test_validator_decision"
|
||||
|
||||
schemaVersionV1 = "v1"
|
||||
)
|
||||
|
||||
// ResponseSchemaDefinition identifies a caller-owned structured response schema asset.
|
||||
type ResponseSchemaDefinition struct {
|
||||
Key ResponseSchemaKey
|
||||
ID string
|
||||
Version string
|
||||
Name string
|
||||
AssetPath string
|
||||
}
|
||||
|
||||
// ResponseSchema describes one registered structured response schema.
|
||||
type ResponseSchema struct {
|
||||
Key ResponseSchemaKey `json:"key"`
|
||||
@@ -35,27 +44,20 @@ type ResponseSchema struct {
|
||||
}
|
||||
|
||||
var responseSchemaRegistry = map[ResponseSchemaKey]ResponseSchema{
|
||||
DNDSpellsSchemaKey: mustLoadResponseSchema(
|
||||
DNDSpellsSchemaKey,
|
||||
"notarius.dnd.spells",
|
||||
schemaVersionV1,
|
||||
"notarius_dnd_spells_v1",
|
||||
"assets/schemas/dnd_spells.v1.json",
|
||||
),
|
||||
TestArtifactSchemaKey: mustLoadResponseSchema(
|
||||
TestArtifactSchemaKey,
|
||||
"notarius.test_artifact",
|
||||
schemaVersionV1,
|
||||
"notarius_test_artifact_v1",
|
||||
"assets/schemas/test_artifact.v1.json",
|
||||
),
|
||||
TestValidatorDecisionSchemaKey: mustLoadResponseSchema(
|
||||
TestValidatorDecisionSchemaKey,
|
||||
"notarius.test_validator_decision",
|
||||
schemaVersionV1,
|
||||
"notarius_test_validator_decision_v1",
|
||||
"assets/schemas/test_validator_decision.v1.json",
|
||||
),
|
||||
TestArtifactSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{
|
||||
Key: TestArtifactSchemaKey,
|
||||
ID: "notarius.test_artifact",
|
||||
Version: schemaVersionV1,
|
||||
Name: "notarius_test_artifact_v1",
|
||||
AssetPath: "assets/schemas/test_artifact.v1.json",
|
||||
}),
|
||||
TestValidatorDecisionSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{
|
||||
Key: TestValidatorDecisionSchemaKey,
|
||||
ID: "notarius.test_validator_decision",
|
||||
Version: schemaVersionV1,
|
||||
Name: "notarius_test_validator_decision_v1",
|
||||
AssetPath: "assets/schemas/test_validator_decision.v1.json",
|
||||
}),
|
||||
}
|
||||
|
||||
// RegisteredResponseSchemas returns all registered response schemas sorted by key.
|
||||
@@ -102,40 +104,35 @@ func (s ResponseSchema) DiagnosticsMap() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func mustLoadResponseSchema(
|
||||
key ResponseSchemaKey,
|
||||
id string,
|
||||
version string,
|
||||
name string,
|
||||
path string,
|
||||
) ResponseSchema {
|
||||
key = ResponseSchemaKey(strings.TrimSpace(string(key)))
|
||||
id = strings.TrimSpace(id)
|
||||
version = strings.TrimSpace(version)
|
||||
name = strings.TrimSpace(name)
|
||||
path = strings.TrimSpace(path)
|
||||
// LoadResponseSchema loads a structured response schema from a caller-owned filesystem.
|
||||
func LoadResponseSchema(fsys fs.FS, def ResponseSchemaDefinition) (ResponseSchema, error) {
|
||||
key := ResponseSchemaKey(strings.TrimSpace(string(def.Key)))
|
||||
id := strings.TrimSpace(def.ID)
|
||||
version := strings.TrimSpace(def.Version)
|
||||
name := strings.TrimSpace(def.Name)
|
||||
path := strings.TrimSpace(def.AssetPath)
|
||||
if key == "" {
|
||||
panic("response schema key must not be empty")
|
||||
return ResponseSchema{}, fmt.Errorf("response schema key must not be empty")
|
||||
}
|
||||
if id == "" {
|
||||
panic("response schema id must not be empty")
|
||||
return ResponseSchema{}, fmt.Errorf("response schema id must not be empty")
|
||||
}
|
||||
if version == "" {
|
||||
panic("response schema version must not be empty")
|
||||
return ResponseSchema{}, fmt.Errorf("response schema version must not be empty")
|
||||
}
|
||||
if name == "" {
|
||||
panic("response schema name must not be empty")
|
||||
return ResponseSchema{}, fmt.Errorf("response schema name must not be empty")
|
||||
}
|
||||
if path == "" {
|
||||
panic("response schema asset path must not be empty")
|
||||
return ResponseSchema{}, fmt.Errorf("response schema asset path must not be empty")
|
||||
}
|
||||
|
||||
rawSchema, err := schemaAssets.ReadFile(path)
|
||||
rawSchema, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("read response schema %s: %v", path, err))
|
||||
return ResponseSchema{}, fmt.Errorf("read response schema %s: %w", path, err)
|
||||
}
|
||||
if !json.Valid(rawSchema) {
|
||||
panic(fmt.Sprintf("response schema %s is not valid JSON", path))
|
||||
return ResponseSchema{}, fmt.Errorf("response schema %s is not valid JSON", path)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(rawSchema)
|
||||
@@ -146,7 +143,15 @@ func mustLoadResponseSchema(
|
||||
Name: name,
|
||||
JSONSchema: append(json.RawMessage(nil), rawSchema...),
|
||||
SHA256: "sha256:" + hex.EncodeToString(hash[:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mustLoadResponseSchema(fsys fs.FS, def ResponseSchemaDefinition) ResponseSchema {
|
||||
schema, err := LoadResponseSchema(fsys, def)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
func cloneResponseSchema(in ResponseSchema) ResponseSchema {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
func TestLookupResponseSchemaSucceedsForRegisteredSchemas(t *testing.T) {
|
||||
tests := []ResponseSchemaKey{
|
||||
DNDSpellsSchemaKey,
|
||||
TestArtifactSchemaKey,
|
||||
TestValidatorDecisionSchemaKey,
|
||||
}
|
||||
@@ -39,6 +38,12 @@ func TestLookupResponseSchemaUnknownReturnsFalse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupResponseSchemaDNDSpellsIsNotFrameworkRegistered(t *testing.T) {
|
||||
if schema, ok := LookupResponseSchema("dnd_spells"); ok {
|
||||
t.Fatalf("expected D&D spells schema lookup to fail in framework registry, got %+v", schema)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustLookupResponseSchemaPanicsForUnknownKey(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
@@ -51,8 +56,8 @@ func TestMustLookupResponseSchemaPanicsForUnknownKey(t *testing.T) {
|
||||
|
||||
func TestRegisteredResponseSchemasSortedByKey(t *testing.T) {
|
||||
schemas := RegisteredResponseSchemas()
|
||||
if len(schemas) != 3 {
|
||||
t.Fatalf("expected three schemas, got %d", len(schemas))
|
||||
if len(schemas) != 2 {
|
||||
t.Fatalf("expected two schemas, got %d", len(schemas))
|
||||
}
|
||||
|
||||
keys := make([]string, len(schemas))
|
||||
@@ -64,8 +69,8 @@ func TestRegisteredResponseSchemasSortedByKey(t *testing.T) {
|
||||
if !sort.StringsAreSorted(keys) {
|
||||
t.Fatalf("expected sorted keys, got %v", keys)
|
||||
}
|
||||
if !seen[DNDSpellsSchemaKey] {
|
||||
t.Fatalf("registered schemas = %v, want %q", keys, DNDSpellsSchemaKey)
|
||||
if !seen[TestArtifactSchemaKey] || !seen[TestValidatorDecisionSchemaKey] {
|
||||
t.Fatalf("registered schemas = %v, want test schemas", keys)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +83,7 @@ func TestResponseSchemaContentIsValidJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
|
||||
for _, key := range []ResponseSchemaKey{DNDSpellsSchemaKey, TestArtifactSchemaKey} {
|
||||
for _, key := range []ResponseSchemaKey{TestArtifactSchemaKey, TestValidatorDecisionSchemaKey} {
|
||||
t.Run(string(key), func(t *testing.T) {
|
||||
first := MustLookupResponseSchema(key)
|
||||
first.JSONSchema[0] = '['
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -17,7 +18,6 @@ var embeddedAssets embed.FS
|
||||
const (
|
||||
SourceBuiltin = "builtin"
|
||||
VersionV1 = "v1"
|
||||
DNDSpellsPromptID = "dnd.spells"
|
||||
TestGenericPromptID = "test.generic"
|
||||
)
|
||||
|
||||
@@ -41,21 +41,23 @@ func (m Metadata) DiagnosticsMap() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
type definition struct {
|
||||
id string
|
||||
version string
|
||||
embeddedDir string
|
||||
systemPath string
|
||||
userPath string
|
||||
// Definition identifies a caller-owned system/user prompt bundle.
|
||||
type Definition struct {
|
||||
PromptID string
|
||||
Version string
|
||||
EmbeddedPath string
|
||||
SystemPath string
|
||||
UserPath string
|
||||
}
|
||||
|
||||
type compiledPrompt struct {
|
||||
// Bundle is a compiled system/user prompt pair.
|
||||
type Bundle struct {
|
||||
systemTmpl *template.Template
|
||||
userTmpl *template.Template
|
||||
metadata Metadata
|
||||
}
|
||||
|
||||
var promptRegistry map[string]compiledPrompt
|
||||
var promptRegistry map[string]*Bundle
|
||||
var sharedHardening string
|
||||
|
||||
func init() {
|
||||
@@ -65,30 +67,23 @@ func init() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defs := []definition{
|
||||
defs := []Definition{
|
||||
{
|
||||
id: DNDSpellsPromptID,
|
||||
version: VersionV1,
|
||||
embeddedDir: "assets/dnd/spells",
|
||||
systemPath: "assets/dnd/spells/system.md",
|
||||
userPath: "assets/dnd/spells/user.md",
|
||||
},
|
||||
{
|
||||
id: TestGenericPromptID,
|
||||
version: VersionV1,
|
||||
embeddedDir: "assets/test/generic",
|
||||
systemPath: "assets/test/generic/system.md",
|
||||
userPath: "assets/test/generic/user.md",
|
||||
PromptID: TestGenericPromptID,
|
||||
Version: VersionV1,
|
||||
EmbeddedPath: "assets/test/generic",
|
||||
SystemPath: "assets/test/generic/system.md",
|
||||
UserPath: "assets/test/generic/user.md",
|
||||
},
|
||||
}
|
||||
|
||||
promptRegistry = make(map[string]compiledPrompt, len(defs))
|
||||
promptRegistry = make(map[string]*Bundle, len(defs))
|
||||
for _, def := range defs {
|
||||
compiled, compileErr := compilePrompt(def)
|
||||
compiled, compileErr := LoadBundle(embeddedAssets, def)
|
||||
if compileErr != nil {
|
||||
panic(compileErr)
|
||||
}
|
||||
promptRegistry[def.id] = compiled
|
||||
promptRegistry[compiled.metadata.PromptID] = compiled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,51 +133,68 @@ func readAsset(assetPath string) (string, error) {
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func compilePrompt(def definition) (compiledPrompt, error) {
|
||||
if strings.TrimSpace(def.id) == "" {
|
||||
return compiledPrompt{}, fmt.Errorf("prompt id must not be empty")
|
||||
// LoadBundle compiles a system/user prompt bundle from a caller-owned filesystem.
|
||||
func LoadBundle(fsys fs.FS, def Definition) (*Bundle, error) {
|
||||
promptID := strings.TrimSpace(def.PromptID)
|
||||
version := strings.TrimSpace(def.Version)
|
||||
embeddedPath := strings.TrimSpace(def.EmbeddedPath)
|
||||
systemPath := strings.TrimSpace(def.SystemPath)
|
||||
userPath := strings.TrimSpace(def.UserPath)
|
||||
if promptID == "" {
|
||||
return nil, fmt.Errorf("prompt id must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(def.version) == "" {
|
||||
return compiledPrompt{}, fmt.Errorf("prompt version must not be empty")
|
||||
if version == "" {
|
||||
return nil, fmt.Errorf("prompt version must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(def.embeddedDir) == "" {
|
||||
return compiledPrompt{}, fmt.Errorf("prompt embedded path must not be empty")
|
||||
if embeddedPath == "" {
|
||||
return nil, fmt.Errorf("prompt embedded path must not be empty")
|
||||
}
|
||||
|
||||
systemSource, err := readAsset(def.systemPath)
|
||||
systemSource, err := readPromptAsset(fsys, systemPath)
|
||||
if err != nil {
|
||||
return compiledPrompt{}, err
|
||||
return nil, err
|
||||
}
|
||||
userSource, err := readAsset(def.userPath)
|
||||
userSource, err := readPromptAsset(fsys, userPath)
|
||||
if err != nil {
|
||||
return compiledPrompt{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"hardening": func() string { return sharedHardening },
|
||||
}
|
||||
systemTmpl, err := template.New(path.Base(def.systemPath)).Option("missingkey=error").Funcs(funcs).Parse(systemSource)
|
||||
systemTmpl, err := template.New(path.Base(systemPath)).Option("missingkey=error").Funcs(funcs).Parse(systemSource)
|
||||
if err != nil {
|
||||
return compiledPrompt{}, fmt.Errorf("parse embedded system prompt %q: %w", def.systemPath, err)
|
||||
return nil, fmt.Errorf("parse embedded system prompt %q: %w", systemPath, err)
|
||||
}
|
||||
userTmpl, err := template.New(path.Base(def.userPath)).Option("missingkey=error").Funcs(funcs).Parse(userSource)
|
||||
userTmpl, err := template.New(path.Base(userPath)).Option("missingkey=error").Funcs(funcs).Parse(userSource)
|
||||
if err != nil {
|
||||
return compiledPrompt{}, fmt.Errorf("parse embedded user prompt %q: %w", def.userPath, err)
|
||||
return nil, fmt.Errorf("parse embedded user prompt %q: %w", userPath, err)
|
||||
}
|
||||
|
||||
hashInput := systemSource + "\n\n" + userSource
|
||||
hash := sha256.Sum256([]byte(hashInput))
|
||||
metadata := Metadata{
|
||||
PromptID: strings.TrimSpace(def.id),
|
||||
PromptVersion: strings.TrimSpace(def.version),
|
||||
PromptID: promptID,
|
||||
PromptVersion: version,
|
||||
PromptSource: SourceBuiltin,
|
||||
EmbeddedPath: strings.TrimSpace(def.embeddedDir),
|
||||
EmbeddedPath: embeddedPath,
|
||||
SHA256: "sha256:" + hex.EncodeToString(hash[:]),
|
||||
}
|
||||
|
||||
return compiledPrompt{
|
||||
return &Bundle{
|
||||
systemTmpl: systemTmpl,
|
||||
userTmpl: userTmpl,
|
||||
metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readPromptAsset(fsys fs.FS, assetPath string) (string, error) {
|
||||
if strings.TrimSpace(assetPath) == "" {
|
||||
return "", fmt.Errorf("prompt asset path must not be empty")
|
||||
}
|
||||
content, err := fs.ReadFile(fsys, assetPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read embedded prompt asset %q: %w", assetPath, err)
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ func TestLookupMetadataSucceedsForRegisteredPrompts(t *testing.T) {
|
||||
promptID string
|
||||
embeddedPath string
|
||||
}{
|
||||
{promptID: DNDSpellsPromptID, embeddedPath: "assets/dnd/spells"},
|
||||
{promptID: TestGenericPromptID, embeddedPath: "assets/test/generic"},
|
||||
}
|
||||
|
||||
@@ -59,8 +58,8 @@ func TestMustLookupMetadataPanicsForUnknownPromptID(t *testing.T) {
|
||||
|
||||
func TestRegisteredMetadataSortedByPromptID(t *testing.T) {
|
||||
registered := RegisteredMetadata()
|
||||
if len(registered) != 2 {
|
||||
t.Fatalf("expected two registered prompts, got %d", len(registered))
|
||||
if len(registered) != 1 {
|
||||
t.Fatalf("expected one registered prompt, got %d", len(registered))
|
||||
}
|
||||
|
||||
ids := make([]string, len(registered))
|
||||
@@ -72,8 +71,8 @@ func TestRegisteredMetadataSortedByPromptID(t *testing.T) {
|
||||
if !sort.StringsAreSorted(ids) {
|
||||
t.Fatalf("expected sorted prompt IDs, got %v", ids)
|
||||
}
|
||||
if !seen[DNDSpellsPromptID] {
|
||||
t.Fatalf("registered prompt IDs = %v, want %q", ids, DNDSpellsPromptID)
|
||||
if !seen[TestGenericPromptID] {
|
||||
t.Fatalf("registered prompt IDs = %v, want %q", ids, TestGenericPromptID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,23 @@ func RenderUserSystem(promptID string, data any) (system string, user string, me
|
||||
if !ok {
|
||||
return "", "", Metadata{}, fmt.Errorf("unknown prompt id %q", promptID)
|
||||
}
|
||||
return compiled.RenderUserSystem(data)
|
||||
}
|
||||
|
||||
// RenderUserSystem renders the bundle's system and user prompts.
|
||||
func (b *Bundle) RenderUserSystem(data any) (system string, user string, metadata Metadata, err error) {
|
||||
if b == nil {
|
||||
return "", "", Metadata{}, fmt.Errorf("prompt bundle must not be nil")
|
||||
}
|
||||
var systemBuf bytes.Buffer
|
||||
if err := compiled.systemTmpl.Execute(&systemBuf, data); err != nil {
|
||||
return "", "", Metadata{}, fmt.Errorf("render system prompt %q: %w", trimmedID, err)
|
||||
if err := b.systemTmpl.Execute(&systemBuf, data); err != nil {
|
||||
return "", "", Metadata{}, fmt.Errorf("render system prompt %q: %w", b.metadata.PromptID, err)
|
||||
}
|
||||
|
||||
var userBuf bytes.Buffer
|
||||
if err := compiled.userTmpl.Execute(&userBuf, data); err != nil {
|
||||
return "", "", Metadata{}, fmt.Errorf("render user prompt %q: %w", trimmedID, err)
|
||||
if err := b.userTmpl.Execute(&userBuf, data); err != nil {
|
||||
return "", "", Metadata{}, fmt.Errorf("render user prompt %q: %w", b.metadata.PromptID, err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(systemBuf.String()), strings.TrimSpace(userBuf.String()), compiled.metadata, nil
|
||||
return strings.TrimSpace(systemBuf.String()), strings.TrimSpace(userBuf.String()), b.metadata, nil
|
||||
}
|
||||
|
||||
@@ -64,47 +64,3 @@ func TestRenderUserSystemIncludesHardeningText(t *testing.T) {
|
||||
t.Fatalf("expected rendered system prompt to include hardening text: %q", system)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderDNDSpellsPromptIncludesHardeningText(t *testing.T) {
|
||||
system, user, metadata, err := RenderUserSystem(DNDSpellsPromptID, map[string]any{
|
||||
"SourceID": "session-alpha",
|
||||
"HasChunk": true,
|
||||
"ChunkID": "session-alpha:chunk:0",
|
||||
"ChunkIndex": 0,
|
||||
"Units": []map[string]any{
|
||||
{
|
||||
"ID": "seg-001",
|
||||
"Text": "Aria casts Cure Wounds.",
|
||||
"Metadata": []map[string]string{
|
||||
{"Key": "speaker", "Value": "Alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RenderUserSystem: %v", err)
|
||||
}
|
||||
|
||||
hardening := strings.TrimSpace(HardeningText())
|
||||
if hardening == "" {
|
||||
t.Fatalf("expected hardening text")
|
||||
}
|
||||
if !strings.Contains(system, hardening) {
|
||||
t.Fatalf("expected rendered system prompt to include hardening text: %q", system)
|
||||
}
|
||||
for _, want := range []string{"session-alpha", "session-alpha:chunk:0", "seg-001", "Aria casts Cure Wounds.", "speaker: Alice"} {
|
||||
if !strings.Contains(user, want) {
|
||||
t.Fatalf("rendered user prompt = %q, want substring %q", user, want)
|
||||
}
|
||||
}
|
||||
if metadata.PromptID != DNDSpellsPromptID {
|
||||
t.Fatalf("unexpected metadata: %+v", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderDNDSpellsPromptMissingTemplateDataReturnsError(t *testing.T) {
|
||||
_, _, _, err := RenderUserSystem(DNDSpellsPromptID, map[string]any{})
|
||||
if err == nil || !strings.Contains(err.Error(), "SourceID") {
|
||||
t.Fatalf("expected missing SourceID error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
6
internal/modules/extract/dnd/spells/assets.go
Normal file
6
internal/modules/extract/dnd/spells/assets.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package spells
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets/prompts/*.md assets/schemas/*.json
|
||||
var embeddedAssets embed.FS
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -80,9 +79,9 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest
|
||||
if err != nil {
|
||||
return contracts.ExtractionResult{}, extractorErrorf("render prompt: %w", err)
|
||||
}
|
||||
schema, ok := llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)
|
||||
if !ok {
|
||||
return contracts.ExtractionResult{}, extractorErrorf("lookup response schema %q", llm.DNDSpellsSchemaKey)
|
||||
schema, err := loadResponseSchema()
|
||||
if err != nil {
|
||||
return contracts.ExtractionResult{}, extractorErrorf("load response schema %q: %w", ResponseSchemaKey, err)
|
||||
}
|
||||
|
||||
var response extractionResponse
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
)
|
||||
|
||||
func TestExtractReturnsSpellCandidateFromStructuredOutput(t *testing.T) {
|
||||
@@ -42,7 +41,10 @@ func TestExtractReturnsSpellCandidateFromStructuredOutput(t *testing.T) {
|
||||
if req.StageName != Key {
|
||||
t.Fatalf("StageName = %q, want %q", req.StageName, Key)
|
||||
}
|
||||
schema := llm.MustLookupResponseSchema(llm.DNDSpellsSchemaKey)
|
||||
schema, err := loadResponseSchema()
|
||||
if err != nil {
|
||||
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
||||
}
|
||||
if req.ResponseSchemaName != schema.Name {
|
||||
t.Fatalf("ResponseSchemaName = %q, want %q", req.ResponseSchemaName, schema.Name)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,22 @@ type promptMetadata struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
var spellsPromptBundle = mustLoadPromptBundle()
|
||||
|
||||
func mustLoadPromptBundle() *prompt.Bundle {
|
||||
bundle, err := prompt.LoadBundle(embeddedAssets, prompt.Definition{
|
||||
PromptID: PromptID,
|
||||
Version: SchemaVersion,
|
||||
EmbeddedPath: "assets/prompts",
|
||||
SystemPath: "assets/prompts/system.md",
|
||||
UserPath: "assets/prompts/user.md",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
func buildPromptData(req contracts.ExtractionRequest) (promptData, error) {
|
||||
if req.Source == nil {
|
||||
return promptData{}, fmt.Errorf("dnd spells prompt: source must not be nil")
|
||||
@@ -58,7 +74,7 @@ func renderPrompt(req contracts.ExtractionRequest) (system string, user string,
|
||||
if err != nil {
|
||||
return "", "", prompt.Metadata{}, err
|
||||
}
|
||||
system, user, metadata, err = prompt.RenderUserSystem(prompt.DNDSpellsPromptID, data)
|
||||
system, user, metadata, err = spellsPromptBundle.RenderUserSystem(data)
|
||||
if err != nil {
|
||||
return "", "", prompt.Metadata{}, fmt.Errorf("dnd spells prompt: %w", err)
|
||||
}
|
||||
|
||||
@@ -92,8 +92,14 @@ func TestRenderPromptIncludesSourceContext(t *testing.T) {
|
||||
t.Fatalf("user prompt = %q, want substring %q", user, want)
|
||||
}
|
||||
}
|
||||
if metadata.PromptID != prompt.DNDSpellsPromptID {
|
||||
t.Fatalf("metadata.PromptID = %q, want %q", metadata.PromptID, prompt.DNDSpellsPromptID)
|
||||
if metadata.PromptID != PromptID {
|
||||
t.Fatalf("metadata.PromptID = %q, want %q", metadata.PromptID, PromptID)
|
||||
}
|
||||
if metadata.PromptVersion != SchemaVersion {
|
||||
t.Fatalf("metadata.PromptVersion = %q, want %q", metadata.PromptVersion, SchemaVersion)
|
||||
}
|
||||
if metadata.EmbeddedPath != "assets/prompts" {
|
||||
t.Fatalf("metadata.EmbeddedPath = %q, want assets/prompts", metadata.EmbeddedPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
internal/modules/extract/dnd/spells/schema.go
Normal file
20
internal/modules/extract/dnd/spells/schema.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package spells
|
||||
|
||||
import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
|
||||
const (
|
||||
PromptID = "dnd.spells"
|
||||
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_spells")
|
||||
ResponseSchemaID = "notarius.dnd.spells"
|
||||
ResponseSchemaName = "notarius_dnd_spells_v1"
|
||||
)
|
||||
|
||||
func loadResponseSchema() (llm.ResponseSchema, error) {
|
||||
return llm.LoadResponseSchema(embeddedAssets, llm.ResponseSchemaDefinition{
|
||||
Key: ResponseSchemaKey,
|
||||
ID: ResponseSchemaID,
|
||||
Version: SchemaVersion,
|
||||
Name: ResponseSchemaName,
|
||||
AssetPath: "assets/schemas/dnd_spells.v1.json",
|
||||
})
|
||||
}
|
||||
@@ -4,23 +4,24 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
)
|
||||
|
||||
func TestLookupResponseSchemaForSpells(t *testing.T) {
|
||||
schema, ok := llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)
|
||||
if !ok {
|
||||
t.Fatalf("LookupResponseSchema(%q) ok = false, want true", llm.DNDSpellsSchemaKey)
|
||||
func TestLoadResponseSchemaForSpells(t *testing.T) {
|
||||
schema, err := loadResponseSchema()
|
||||
if err != nil {
|
||||
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
||||
}
|
||||
if schema.ID != "notarius.dnd.spells" {
|
||||
t.Fatalf("schema.ID = %q, want notarius.dnd.spells", schema.ID)
|
||||
if schema.Key != ResponseSchemaKey {
|
||||
t.Fatalf("schema.Key = %q, want %q", schema.Key, ResponseSchemaKey)
|
||||
}
|
||||
if schema.ID != ResponseSchemaID {
|
||||
t.Fatalf("schema.ID = %q, want %q", schema.ID, ResponseSchemaID)
|
||||
}
|
||||
if schema.Version != SchemaVersion {
|
||||
t.Fatalf("schema.Version = %q, want %q", schema.Version, SchemaVersion)
|
||||
}
|
||||
if schema.Name != "notarius_dnd_spells_v1" {
|
||||
t.Fatalf("schema.Name = %q, want notarius_dnd_spells_v1", schema.Name)
|
||||
if schema.Name != ResponseSchemaName {
|
||||
t.Fatalf("schema.Name = %q, want %q", schema.Name, ResponseSchemaName)
|
||||
}
|
||||
if !strings.HasPrefix(schema.SHA256, "sha256:") {
|
||||
t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256)
|
||||
@@ -30,12 +31,34 @@ func TestLookupResponseSchemaForSpells(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
|
||||
first, err := loadResponseSchema()
|
||||
if err != nil {
|
||||
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
||||
}
|
||||
first.JSONSchema[0] = '['
|
||||
|
||||
second, err := loadResponseSchema()
|
||||
if err != nil {
|
||||
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
||||
}
|
||||
if !json.Valid(second.JSONSchema) {
|
||||
t.Fatalf("schema JSON was mutated: %s", second.JSONSchema)
|
||||
}
|
||||
if len(second.JSONSchema) > 0 && second.JSONSchema[0] == '[' {
|
||||
t.Fatalf("schema JSON did not use defensive copy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseSchemaDiagnosticsOmitRawSchema(t *testing.T) {
|
||||
schema := llm.MustLookupResponseSchema(llm.DNDSpellsSchemaKey)
|
||||
schema, err := loadResponseSchema()
|
||||
if err != nil {
|
||||
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
|
||||
}
|
||||
diagnostics := schema.DiagnosticsMap()
|
||||
|
||||
if diagnostics["key"] != llm.DNDSpellsSchemaKey {
|
||||
t.Fatalf("diagnostics[key] = %#v, want %q", diagnostics["key"], llm.DNDSpellsSchemaKey)
|
||||
if diagnostics["key"] != ResponseSchemaKey {
|
||||
t.Fatalf("diagnostics[key] = %#v, want %q", diagnostics["key"], ResponseSchemaKey)
|
||||
}
|
||||
for _, key := range []string{"id", "version", "name", "sha256"} {
|
||||
if diagnostics[key] == "" {
|
||||
@@ -45,4 +68,7 @@ func TestResponseSchemaDiagnosticsOmitRawSchema(t *testing.T) {
|
||||
if _, ok := diagnostics["json_schema"]; ok {
|
||||
t.Fatalf("diagnostics should omit raw schema content: %#v", diagnostics)
|
||||
}
|
||||
if _, ok := diagnostics["JSONSchema"]; ok {
|
||||
t.Fatalf("diagnostics should omit raw schema content: %#v", diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user