Add prompt reference template functions
This commit is contained in:
@@ -44,6 +44,12 @@ CLI bindings resolve relative to the current working directory, and materialized
|
||||
reference content is passed to extractors through `ExtractionRequest`.
|
||||
Reference content is omitted from diagnostics and manifests.
|
||||
|
||||
Prompt bundles can declare reference slots and use `reference` and
|
||||
`hasreference` template functions. Bundle loading validates string-literal slot
|
||||
names against the declaration. Rendering receives a lane reference set from the
|
||||
caller; unbound optional slots render as empty strings, and `hasreference`
|
||||
returns true only when at least one bound item has content.
|
||||
|
||||
## Registries And Module Specs
|
||||
|
||||
`pipeline.Registries` holds concrete constructors for execution. A
|
||||
|
||||
@@ -7,9 +7,13 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"text/template/parse"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
//go:embed assets/**
|
||||
@@ -43,18 +47,20 @@ func (m Metadata) DiagnosticsMap() map[string]any {
|
||||
|
||||
// Definition identifies a caller-owned system/user prompt bundle.
|
||||
type Definition struct {
|
||||
PromptID string
|
||||
Version string
|
||||
EmbeddedPath string
|
||||
SystemPath string
|
||||
UserPath string
|
||||
PromptID string
|
||||
Version string
|
||||
EmbeddedPath string
|
||||
SystemPath string
|
||||
UserPath string
|
||||
ReferenceSlots []contracts.ReferenceSlot
|
||||
}
|
||||
|
||||
// Bundle is a compiled system/user prompt pair.
|
||||
type Bundle struct {
|
||||
systemTmpl *template.Template
|
||||
userTmpl *template.Template
|
||||
metadata Metadata
|
||||
systemTmpl *template.Template
|
||||
userTmpl *template.Template
|
||||
metadata Metadata
|
||||
referenceSlots map[string]contracts.ReferenceSlot
|
||||
}
|
||||
|
||||
// Metadata returns metadata for the compiled prompt bundle.
|
||||
@@ -168,7 +174,9 @@ func LoadBundle(fsys fs.FS, def Definition) (*Bundle, error) {
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"hardening": func() string { return sharedHardening },
|
||||
"hardening": func() string { return sharedHardening },
|
||||
"reference": func(string) (string, error) { return "", nil },
|
||||
"hasreference": func(string) (bool, error) { return false, nil },
|
||||
}
|
||||
systemTmpl, err := template.New(path.Base(systemPath)).Option("missingkey=error").Funcs(funcs).Parse(systemSource)
|
||||
if err != nil {
|
||||
@@ -178,6 +186,13 @@ func LoadBundle(fsys fs.FS, def Definition) (*Bundle, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse embedded user prompt %q: %w", userPath, err)
|
||||
}
|
||||
referenceSlots := referenceSlotMap(def.ReferenceSlots)
|
||||
if err := validateTemplateReferenceSlots(systemTmpl, referenceSlots); err != nil {
|
||||
return nil, fmt.Errorf("validate embedded system prompt %q: %w", systemPath, err)
|
||||
}
|
||||
if err := validateTemplateReferenceSlots(userTmpl, referenceSlots); err != nil {
|
||||
return nil, fmt.Errorf("validate embedded user prompt %q: %w", userPath, err)
|
||||
}
|
||||
|
||||
hashInput := systemSource + "\n\n" + userSource
|
||||
hash := sha256.Sum256([]byte(hashInput))
|
||||
@@ -190,12 +205,127 @@ func LoadBundle(fsys fs.FS, def Definition) (*Bundle, error) {
|
||||
}
|
||||
|
||||
return &Bundle{
|
||||
systemTmpl: systemTmpl,
|
||||
userTmpl: userTmpl,
|
||||
metadata: metadata,
|
||||
systemTmpl: systemTmpl,
|
||||
userTmpl: userTmpl,
|
||||
metadata: metadata,
|
||||
referenceSlots: referenceSlots,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func referenceSlotMap(slots []contracts.ReferenceSlot) map[string]contracts.ReferenceSlot {
|
||||
if len(slots) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]contracts.ReferenceSlot, len(slots))
|
||||
for _, slot := range slots {
|
||||
name := strings.TrimSpace(slot.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
slot.Name = name
|
||||
slot.AcceptedMediaTypes = append([]string(nil), slot.AcceptedMediaTypes...)
|
||||
out[name] = slot
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateTemplateReferenceSlots(tmpl *template.Template, declared map[string]contracts.ReferenceSlot) error {
|
||||
if tmpl == nil || tmpl.Tree == nil || tmpl.Tree.Root == nil {
|
||||
return nil
|
||||
}
|
||||
return validateReferenceNodes(tmpl.Tree.Root, declared)
|
||||
}
|
||||
|
||||
func validateReferenceNodes(node parse.Node, declared map[string]contracts.ReferenceSlot) error {
|
||||
if node == nil || reflect.ValueOf(node).IsNil() {
|
||||
return nil
|
||||
}
|
||||
switch typed := node.(type) {
|
||||
case *parse.ListNode:
|
||||
for _, child := range typed.Nodes {
|
||||
if err := validateReferenceNodes(child, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case *parse.ActionNode:
|
||||
return validateReferencePipeline(typed.Pipe, declared)
|
||||
case *parse.IfNode:
|
||||
if err := validateReferencePipeline(typed.Pipe, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReferenceNodes(typed.List, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateReferenceNodes(typed.ElseList, declared)
|
||||
case *parse.RangeNode:
|
||||
if err := validateReferencePipeline(typed.Pipe, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReferenceNodes(typed.List, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateReferenceNodes(typed.ElseList, declared)
|
||||
case *parse.WithNode:
|
||||
if err := validateReferencePipeline(typed.Pipe, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReferenceNodes(typed.List, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateReferenceNodes(typed.ElseList, declared)
|
||||
case *parse.TemplateNode:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateReferencePipeline(pipe *parse.PipeNode, declared map[string]contracts.ReferenceSlot) error {
|
||||
if pipe == nil {
|
||||
return nil
|
||||
}
|
||||
for _, cmd := range pipe.Cmds {
|
||||
if err := validateReferenceCommand(cmd, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateReferenceCommand(cmd *parse.CommandNode, declared map[string]contracts.ReferenceSlot) error {
|
||||
if cmd == nil || len(cmd.Args) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, arg := range cmd.Args[1:] {
|
||||
if nested, ok := arg.(*parse.PipeNode); ok {
|
||||
if err := validateReferencePipeline(nested, declared); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
identifier, ok := cmd.Args[0].(*parse.IdentifierNode)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if identifier.Ident != "reference" && identifier.Ident != "hasreference" {
|
||||
return nil
|
||||
}
|
||||
if len(cmd.Args) != 2 {
|
||||
return fmt.Errorf("%s requires one string slot name", identifier.Ident)
|
||||
}
|
||||
slotArg, ok := cmd.Args[1].(*parse.StringNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s requires a string literal slot name", identifier.Ident)
|
||||
}
|
||||
slotName := strings.TrimSpace(slotArg.Text)
|
||||
if slotName == "" {
|
||||
return fmt.Errorf("%s slot name must not be empty", identifier.Ident)
|
||||
}
|
||||
if _, ok := declared[slotName]; !ok {
|
||||
return fmt.Errorf("%s slot %q is not declared", identifier.Ident, slotName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPromptAsset(fsys fs.FS, assetPath string) (string, error) {
|
||||
if strings.TrimSpace(assetPath) == "" {
|
||||
return "", fmt.Errorf("prompt asset path must not be empty")
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
// RenderUserSystem renders the system and user prompt pair for promptID.
|
||||
@@ -16,20 +19,105 @@ func RenderUserSystem(promptID string, data any) (system string, user string, me
|
||||
return compiled.RenderUserSystem(data)
|
||||
}
|
||||
|
||||
// RenderUserSystemWithReferences renders the system and user prompt pair for promptID with reference template functions.
|
||||
func RenderUserSystemWithReferences(promptID string, data any, references contracts.ReferenceSet) (system string, user string, metadata Metadata, err error) {
|
||||
trimmedID := strings.TrimSpace(promptID)
|
||||
compiled, ok := promptRegistry[trimmedID]
|
||||
if !ok {
|
||||
return "", "", Metadata{}, fmt.Errorf("unknown prompt id %q", promptID)
|
||||
}
|
||||
return compiled.RenderUserSystemWithReferences(data, references)
|
||||
}
|
||||
|
||||
// RenderUserSystem renders the bundle's system and user prompts.
|
||||
func (b *Bundle) RenderUserSystem(data any) (system string, user string, metadata Metadata, err error) {
|
||||
return b.RenderUserSystemWithReferences(data, contracts.ReferenceSet{})
|
||||
}
|
||||
|
||||
// RenderUserSystemWithReferences renders the bundle's system and user prompts with reference template functions.
|
||||
func (b *Bundle) RenderUserSystemWithReferences(data any, references contracts.ReferenceSet) (system string, user string, metadata Metadata, err error) {
|
||||
if b == nil {
|
||||
return "", "", Metadata{}, fmt.Errorf("prompt bundle must not be nil")
|
||||
}
|
||||
systemTmpl, userTmpl, err := b.renderTemplates(references)
|
||||
if err != nil {
|
||||
return "", "", Metadata{}, err
|
||||
}
|
||||
var systemBuf bytes.Buffer
|
||||
if err := b.systemTmpl.Execute(&systemBuf, data); err != nil {
|
||||
if err := 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 := b.userTmpl.Execute(&userBuf, data); err != nil {
|
||||
if err := 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()), b.metadata, nil
|
||||
}
|
||||
|
||||
func (b *Bundle) renderTemplates(references contracts.ReferenceSet) (*template.Template, *template.Template, error) {
|
||||
funcs := b.referenceFuncs(references)
|
||||
systemTmpl, err := b.systemTmpl.Clone()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("clone system prompt %q: %w", b.metadata.PromptID, err)
|
||||
}
|
||||
userTmpl, err := b.userTmpl.Clone()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("clone user prompt %q: %w", b.metadata.PromptID, err)
|
||||
}
|
||||
systemTmpl.Funcs(funcs)
|
||||
userTmpl.Funcs(funcs)
|
||||
return systemTmpl, userTmpl, nil
|
||||
}
|
||||
|
||||
func (b *Bundle) referenceFuncs(references contracts.ReferenceSet) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"hardening": func() string { return sharedHardening },
|
||||
"hasreference": func(slotName string) (bool, error) {
|
||||
items, _, err := b.referenceItems(slotName, references)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, item := range items {
|
||||
if len(item.Content) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
},
|
||||
"reference": func(slotName string) (string, error) {
|
||||
items, slot, err := b.referenceItems(slotName, references)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if len(items) > 1 && !slot.Multiple {
|
||||
return "", fmt.Errorf("reference slot %q has %d bound items but does not allow multiple", slot.Name, len(items))
|
||||
}
|
||||
parts := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
parts = append(parts, string(item.Content))
|
||||
}
|
||||
return strings.Join(parts, "\n"), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bundle) referenceItems(slotName string, references contracts.ReferenceSet) ([]contracts.ReferenceItem, contracts.ReferenceSlot, error) {
|
||||
slotName = strings.TrimSpace(slotName)
|
||||
slot, ok := b.referenceSlots[slotName]
|
||||
if !ok {
|
||||
return nil, contracts.ReferenceSlot{}, fmt.Errorf("reference slot %q is not declared", slotName)
|
||||
}
|
||||
if len(references.Slots) == 0 {
|
||||
return nil, slot, nil
|
||||
}
|
||||
resolved, ok := references.Slots[slotName]
|
||||
if !ok {
|
||||
return nil, slot, nil
|
||||
}
|
||||
return append([]contracts.ReferenceItem(nil), resolved.Items...), slot, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestRenderUserSystemReturnsTextAndMetadata(t *testing.T) {
|
||||
@@ -64,3 +69,175 @@ func TestRenderUserSystemIncludesHardeningText(t *testing.T) {
|
||||
t.Fatalf("expected rendered system prompt to include hardening text: %q", system)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderUserSystemWithReferencesRendersDeclaredSlots(t *testing.T) {
|
||||
bundle := loadReferenceBundle(t,
|
||||
[]contracts.ReferenceSlot{{Name: "roster"}, {Name: "glossary"}},
|
||||
`System has roster={{ hasreference "roster" }} has glossary={{ hasreference "glossary" }}`,
|
||||
`Roster={{ reference "roster" }} Glossary={{ reference "glossary" }}`,
|
||||
)
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"roster": {
|
||||
Slot: contracts.ReferenceSlot{Name: "roster"},
|
||||
Items: []contracts.ReferenceItem{
|
||||
{SlotName: "roster", Content: []byte("Aria")},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
system, user, _, err := bundle.RenderUserSystemWithReferences(map[string]any{}, references)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderUserSystemWithReferences: %v", err)
|
||||
}
|
||||
if !strings.Contains(system, "has roster=true") || !strings.Contains(system, "has glossary=false") {
|
||||
t.Fatalf("system = %q, want reference presence flags", system)
|
||||
}
|
||||
if !strings.Contains(user, "Roster=Aria") || !strings.Contains(user, "Glossary=") {
|
||||
t.Fatalf("user = %q, want rendered and empty optional references", user)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderUserSystemReferenceHasReferenceRequiresContent(t *testing.T) {
|
||||
bundle := loadReferenceBundle(t,
|
||||
[]contracts.ReferenceSlot{{Name: "roster"}},
|
||||
`System`,
|
||||
`{{ hasreference "roster" }} {{ reference "roster" }}`,
|
||||
)
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"roster": {
|
||||
Slot: contracts.ReferenceSlot{Name: "roster"},
|
||||
Items: []contracts.ReferenceItem{
|
||||
{SlotName: "roster", Content: nil},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, user, _, err := bundle.RenderUserSystemWithReferences(map[string]any{}, references)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderUserSystemWithReferences: %v", err)
|
||||
}
|
||||
if user != "false" {
|
||||
t.Fatalf("user = %q, want false with empty reference content", user)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBundleRejectsUndeclaredReferenceSlots(t *testing.T) {
|
||||
_, err := LoadBundle(referenceBundleFS(`System`, `{{ reference "roster" }}`), referenceBundleDefinition(nil))
|
||||
if err == nil || !strings.Contains(err.Error(), "roster") || !strings.Contains(err.Error(), "not declared") {
|
||||
t.Fatalf("LoadBundle() error = %v, want undeclared reference slot error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBundleRejectsDynamicReferenceSlotNames(t *testing.T) {
|
||||
_, err := LoadBundle(referenceBundleFS(`System`, `{{ reference .SlotName }}`), referenceBundleDefinition([]contracts.ReferenceSlot{{Name: "roster"}}))
|
||||
if err == nil || !strings.Contains(err.Error(), "string literal") {
|
||||
t.Fatalf("LoadBundle() error = %v, want string literal error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBundleRejectsNestedUndeclaredReferenceSlots(t *testing.T) {
|
||||
_, err := LoadBundle(referenceBundleFS(`System`, `{{ printf "%s" (reference "roster") }}`), referenceBundleDefinition(nil))
|
||||
if err == nil || !strings.Contains(err.Error(), "roster") || !strings.Contains(err.Error(), "not declared") {
|
||||
t.Fatalf("LoadBundle() error = %v, want nested undeclared reference slot error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderUserSystemRejectsMultipleReferenceItemsUnlessDeclared(t *testing.T) {
|
||||
bundle := loadReferenceBundle(t,
|
||||
[]contracts.ReferenceSlot{{Name: "roster"}},
|
||||
`System`,
|
||||
`{{ reference "roster" }}`,
|
||||
)
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"roster": {
|
||||
Slot: contracts.ReferenceSlot{Name: "roster"},
|
||||
Items: []contracts.ReferenceItem{
|
||||
{SlotName: "roster", Content: []byte("Aria")},
|
||||
{SlotName: "roster", Content: []byte("Bryn")},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, _, _, err := bundle.RenderUserSystemWithReferences(map[string]any{}, references)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not allow multiple") {
|
||||
t.Fatalf("RenderUserSystemWithReferences() error = %v, want multiple item error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderUserSystemRendersMultipleReferenceItemsDeterministicallyWhenDeclared(t *testing.T) {
|
||||
bundle := loadReferenceBundle(t,
|
||||
[]contracts.ReferenceSlot{{Name: "roster", Multiple: true}},
|
||||
`System`,
|
||||
`{{ reference "roster" }}`,
|
||||
)
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"roster": {
|
||||
Slot: contracts.ReferenceSlot{Name: "roster", Multiple: true},
|
||||
Items: []contracts.ReferenceItem{
|
||||
{SlotName: "roster", Content: []byte("Aria")},
|
||||
{SlotName: "roster", Content: []byte("Bryn")},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, first, _, err := bundle.RenderUserSystemWithReferences(map[string]any{}, references)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderUserSystemWithReferences(first): %v", err)
|
||||
}
|
||||
_, second, _, err := bundle.RenderUserSystemWithReferences(map[string]any{}, references)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderUserSystemWithReferences(second): %v", err)
|
||||
}
|
||||
if first != "Aria\nBryn" || first != second {
|
||||
t.Fatalf("rendered references = %q/%q, want deterministic item order", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptMetadataHashIgnoresRenderedReferenceContent(t *testing.T) {
|
||||
systemSource := `System`
|
||||
userSource := `{{ reference "roster" }}`
|
||||
bundle := loadReferenceBundle(t, []contracts.ReferenceSlot{{Name: "roster"}}, systemSource, userSource)
|
||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"roster": {
|
||||
Slot: contracts.ReferenceSlot{Name: "roster"},
|
||||
Items: []contracts.ReferenceItem{{SlotName: "roster", Content: []byte("Aria")}},
|
||||
},
|
||||
}}
|
||||
|
||||
_, _, metadata, err := bundle.RenderUserSystemWithReferences(map[string]any{}, references)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderUserSystemWithReferences: %v", err)
|
||||
}
|
||||
hash := sha256.Sum256([]byte(systemSource + "\n\n" + userSource))
|
||||
want := "sha256:" + hex.EncodeToString(hash[:])
|
||||
if metadata.SHA256 != want {
|
||||
t.Fatalf("metadata.SHA256 = %q, want template source hash %q", metadata.SHA256, want)
|
||||
}
|
||||
}
|
||||
|
||||
func loadReferenceBundle(t *testing.T, slots []contracts.ReferenceSlot, systemSource string, userSource string) *Bundle {
|
||||
t.Helper()
|
||||
bundle, err := LoadBundle(referenceBundleFS(systemSource, userSource), referenceBundleDefinition(slots))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadBundle() error = %v, want nil", err)
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
func referenceBundleDefinition(slots []contracts.ReferenceSlot) Definition {
|
||||
return Definition{
|
||||
PromptID: "test.references",
|
||||
Version: VersionV1,
|
||||
EmbeddedPath: "assets/test/references",
|
||||
SystemPath: "assets/test/references/system.md",
|
||||
UserPath: "assets/test/references/user.md",
|
||||
ReferenceSlots: slots,
|
||||
}
|
||||
}
|
||||
|
||||
func referenceBundleFS(systemSource string, userSource string) fstest.MapFS {
|
||||
return fstest.MapFS{
|
||||
"assets/test/references/system.md": {Data: []byte(systemSource)},
|
||||
"assets/test/references/user.md": {Data: []byte(userSource)},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user