Resolve linked prompts in their host context
This commit is contained in:
@@ -87,6 +87,8 @@ report failures.
|
||||
|
||||
## R2 — P2: Preserve Host Prompt Source Context Through Symlinks
|
||||
|
||||
**Status:** Completed. Prompt imports use the recorded native host context and protect the resolved sources.
|
||||
|
||||
**Category**
|
||||
|
||||
Main-relative resource loading divergence in Stage 3.
|
||||
|
||||
@@ -161,3 +161,40 @@ func TestEngineUsesRecordedContextWithWiderRoot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineResolvesLinkedPromptMessageBesideDefinition(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(root, "defs"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, contents := range map[string]string{
|
||||
"config.yml": "version: 3\n",
|
||||
"defs/resources.yml": "prompts:\n - id: linked\n messages:\n - role: user\n content_file: message.txt\n output:\n format: text\n validation_mode: none\n",
|
||||
"defs/message.txt": "message beside definition",
|
||||
"message.txt": "message beside symlink",
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(root, name), []byte(contents), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Symlink("defs/resources.yml", filepath.Join(root, "prompts.yml")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(root, "config.yml"))}, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime, err := NewRuntime(context.Background(), loaded, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prompt, err := runtime.Engine.InspectPrompt(context.Background(), "linked")
|
||||
if err != nil || prompt.Messages[0].Content != "message beside definition" {
|
||||
t.Fatalf("linked prompt: %+v, %v", prompt, err)
|
||||
}
|
||||
for _, name := range []string{"defs/resources.yml", "defs/message.txt"} {
|
||||
if !slices.Contains(runtime.SourcePaths, filepath.Join(root, name)) {
|
||||
t.Errorf("missing consumed source %s in %v", name, runtime.SourcePaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ type AssetError struct{ Err error }
|
||||
func (e *AssetError) Error() string { return e.Err.Error() }
|
||||
func (e *AssetError) Unwrap() error { return e.Err }
|
||||
|
||||
const operatorFilesystem = "Audita operator prompts"
|
||||
const operatorImports = `config:
|
||||
imports:
|
||||
- path: prompts.yml
|
||||
@@ -57,7 +56,6 @@ func newRuntimeWithPromptFS(ctx context.Context, loaded *ck.Result[config.Config
|
||||
return nil, &AssetError{Err: fmt.Errorf("discover embedded prompts: %w", err)}
|
||||
}
|
||||
sources := pk.ConfigurationSources{Underlays: []ck.Underlay{{Inputs: embedded}}}
|
||||
var hostRoot string
|
||||
for _, source := range loaded.Sources() {
|
||||
if source.Layer != ck.LayerMain || source.ImportedFrom != nil {
|
||||
continue
|
||||
@@ -66,19 +64,24 @@ func newRuntimeWithPromptFS(ctx context.Context, loaded *ck.Result[config.Config
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
filesystem := ck.NewFilesystem(operatorFilesystem, context.FS)
|
||||
sources.Underlays = append(sources.Underlays, ck.Underlay{Inputs: []ck.Input{{
|
||||
Source: ck.Bytes("Audita automatic prompt imports", []byte(operatorImports)), Imports: true,
|
||||
Filesystem: &ck.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: context.BaseDir},
|
||||
}}})
|
||||
var filesystemContext ck.FilesystemContext
|
||||
if source.Kind == ck.SourceKindFile && source.Path != "" {
|
||||
hostRoot = filepath.Dir(source.Path)
|
||||
base := filepath.Dir(source.Path)
|
||||
root := base
|
||||
if context.BaseDir != "." {
|
||||
for range strings.Split(context.BaseDir, "/") {
|
||||
hostRoot = filepath.Dir(hostRoot)
|
||||
root = filepath.Dir(root)
|
||||
}
|
||||
}
|
||||
filesystemContext = ck.FilesystemContext{Root: root, BaseDir: base}
|
||||
} else {
|
||||
filesystem := ck.NewFilesystem("Audita operator prompts", context.FS)
|
||||
filesystemContext = ck.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: context.BaseDir}
|
||||
}
|
||||
sources.Underlays = append(sources.Underlays, ck.Underlay{Inputs: []ck.Input{{
|
||||
Source: ck.Bytes("Audita automatic prompt imports", []byte(operatorImports)), Imports: true,
|
||||
Filesystem: &filesystemContext,
|
||||
}}})
|
||||
break
|
||||
}
|
||||
engine, err := pk.NewEngine(pk.Config{Sources: sources, HTTPClient: client}, pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
||||
@@ -90,7 +93,7 @@ func newRuntimeWithPromptFS(ctx context.Context, loaded *ck.Result[config.Config
|
||||
return nil, embeddedAssetOrInputError(ctx, embedded, fmt.Errorf("inspect prompt %s: %w", meta.PromptID, err))
|
||||
}
|
||||
}
|
||||
paths, err := capturedSourcePaths(engine, hostRoot)
|
||||
paths, err := capturedSourcePaths(engine)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect captured LLM sources: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func capturedSourcePaths(engine *pk.Engine, hostRoot string) ([]string, error) {
|
||||
func capturedSourcePaths(engine *pk.Engine) ([]string, error) {
|
||||
inspection, err := engine.InspectConfiguration()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect configuration: %w", err)
|
||||
@@ -25,9 +25,6 @@ func capturedSourcePaths(engine *pk.Engine, hostRoot string) ([]string, error) {
|
||||
if source.Kind == ck.SourceKindFile {
|
||||
return source.Path
|
||||
}
|
||||
if hostRoot != "" && source.Kind == ck.SourceKindFilesystem && strings.HasPrefix(source.Label, operatorFilesystem+"/") {
|
||||
return filepath.Join(hostRoot, filepath.FromSlash(source.Path))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
for _, source := range sources {
|
||||
|
||||
Reference in New Issue
Block a user