130 lines
4.2 KiB
Go
130 lines
4.2 KiB
Go
package llm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/redaction"
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/responseschema"
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
|
|
ck "go.maximumdirect.net/configkit"
|
|
pk "go.maximumdirect.net/promptkit"
|
|
)
|
|
|
|
// Runtime owns the captured engine shared by all processing clients.
|
|
type Runtime struct {
|
|
Config config.Config
|
|
Engine *pk.Engine
|
|
SourcePaths []string
|
|
nextCallID atomic.Uint64
|
|
}
|
|
|
|
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
|
|
required: false
|
|
- dir: prompts
|
|
required: false
|
|
recursive: true
|
|
pattern: ["*.yml", "*.yaml"]
|
|
`
|
|
|
|
func NewRuntime(ctx context.Context, loaded *ck.Result[config.Config], client *http.Client) (*Runtime, error) {
|
|
return newRuntimeWithPromptFS(ctx, loaded, client, prompts.Filesystem())
|
|
}
|
|
|
|
func newRuntimeWithPromptFS(ctx context.Context, loaded *ck.Result[config.Config], client *http.Client, promptFS fs.FS) (*Runtime, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if loaded == nil {
|
|
return nil, fmt.Errorf("application configuration is required")
|
|
}
|
|
embedded, err := pk.FilesystemInputs("Audita embedded prompts", promptFS, prompts.AssetRoot)
|
|
if err != nil {
|
|
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
|
|
}
|
|
context, ok := loaded.SourceContext(source.ID)
|
|
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},
|
|
}}})
|
|
if source.Kind == ck.SourceKindFile && source.Path != "" {
|
|
hostRoot = filepath.Dir(source.Path)
|
|
if context.BaseDir != "." {
|
|
for range strings.Split(context.BaseDir, "/") {
|
|
hostRoot = filepath.Dir(hostRoot)
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
engine, err := pk.NewEngine(pk.Config{Sources: sources, HTTPClient: client}, pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
|
if err != nil {
|
|
return nil, embeddedAssetOrInputError(ctx, embedded, fmt.Errorf("assemble LLM runtime: %w", err))
|
|
}
|
|
for _, meta := range prompts.RegisteredMetadata() {
|
|
if _, err := engine.InspectPrompt(ctx, meta.PromptID); err != nil {
|
|
return nil, embeddedAssetOrInputError(ctx, embedded, fmt.Errorf("inspect prompt %s: %w", meta.PromptID, err))
|
|
}
|
|
}
|
|
paths, err := capturedSourcePaths(engine, hostRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inspect captured LLM sources: %w", err)
|
|
}
|
|
return &Runtime{Config: loaded.Config, Engine: engine, SourcePaths: paths}, nil
|
|
}
|
|
|
|
func embeddedAssetOrInputError(ctx context.Context, inputs []ck.Input, cause error) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
engine, err := pk.NewEngine(pk.Config{Sources: pk.ConfigurationSources{Underlays: []ck.Underlay{{Inputs: inputs}}}}, pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
|
if err == nil {
|
|
for _, meta := range prompts.RegisteredMetadata() {
|
|
if _, err = engine.InspectPrompt(ctx, meta.PromptID); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return &AssetError{Err: fmt.Errorf("validate embedded prompt assets: %w", err)}
|
|
}
|
|
return cause
|
|
}
|
|
|
|
func IsAssetError(err error) bool { var target *AssetError; return errors.As(err, &target) }
|
|
|
|
func RedactError(err error, c config.Config) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
return redaction.New(ConfiguredSecrets(&c)).String(err.Error())
|
|
}
|