2026-09-24 01:50:10 +00:00
|
|
|
package llm
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-10-04 00:45:23 +00:00
|
|
|
"io/fs"
|
2026-09-24 01:50:10 +00:00
|
|
|
"net/http"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
2026-09-24 02:23:57 +00:00
|
|
|
"sync/atomic"
|
2026-09-24 01:50:10 +00:00
|
|
|
|
2026-10-11 01:40:38 +00:00
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/responseschema"
|
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
|
2026-11-10 16:13:53 +00:00
|
|
|
ck "go.maximumdirect.net/configkit"
|
|
|
|
|
pk "go.maximumdirect.net/promptkit"
|
2026-09-24 01:50:10 +00:00
|
|
|
)
|
|
|
|
|
|
2026-11-10 16:13:53 +00:00
|
|
|
// Runtime owns the captured engine shared by all processing clients.
|
2026-09-24 01:50:10 +00:00
|
|
|
type Runtime struct {
|
2026-11-10 16:13:53 +00:00
|
|
|
Config config.Config
|
|
|
|
|
Engine *pk.Engine
|
2026-10-04 00:56:14 +00:00
|
|
|
SourcePaths []string
|
|
|
|
|
nextCallID atomic.Uint64
|
2026-09-24 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AssetError struct{ Err error }
|
|
|
|
|
|
|
|
|
|
func (e *AssetError) Error() string { return e.Err.Error() }
|
|
|
|
|
func (e *AssetError) Unwrap() error { return e.Err }
|
|
|
|
|
|
2026-11-10 16:13:53 +00:00
|
|
|
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) {
|
2026-11-10 20:35:35 +00:00
|
|
|
engine, err := NewEngine(ctx, loaded, client)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
paths, err := capturedSourcePaths(engine)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("inspect captured LLM sources: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return &Runtime{Config: loaded.Config, Engine: engine, SourcePaths: paths}, nil
|
2026-10-04 00:45:23 +00:00
|
|
|
}
|
|
|
|
|
|
2026-11-10 20:35:35 +00:00
|
|
|
// NewEngine loads the captured PromptKit resources without processing collision checks.
|
|
|
|
|
func NewEngine(ctx context.Context, loaded *ck.Result[config.Config], client *http.Client) (*pk.Engine, error) {
|
|
|
|
|
return newEngineWithPromptFS(ctx, loaded, client, prompts.Filesystem())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newEngineWithPromptFS(ctx context.Context, loaded *ck.Result[config.Config], client *http.Client, promptFS fs.FS) (*pk.Engine, error) {
|
2026-10-04 00:45:23 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
if loaded == nil {
|
|
|
|
|
return nil, fmt.Errorf("application configuration is required")
|
2026-09-24 01:50:10 +00:00
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
embedded, err := pk.FilesystemInputs("Audita embedded prompts", promptFS, prompts.AssetRoot)
|
2026-10-04 00:33:31 +00:00
|
|
|
if err != nil {
|
2026-10-04 00:45:23 +00:00
|
|
|
return nil, &AssetError{Err: fmt.Errorf("discover embedded prompts: %w", err)}
|
2026-09-24 01:50:10 +00:00
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
sources := pk.ConfigurationSources{Underlays: []ck.Underlay{{Inputs: embedded}}}
|
|
|
|
|
for _, source := range loaded.Sources() {
|
|
|
|
|
if source.Layer != ck.LayerMain || source.ImportedFrom != nil {
|
|
|
|
|
continue
|
2026-10-04 00:45:23 +00:00
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
context, ok := loaded.SourceContext(source.ID)
|
|
|
|
|
if !ok {
|
|
|
|
|
break
|
2026-10-04 00:33:31 +00:00
|
|
|
}
|
2026-11-10 20:17:54 +00:00
|
|
|
var filesystemContext ck.FilesystemContext
|
2026-11-10 16:13:53 +00:00
|
|
|
if source.Kind == ck.SourceKindFile && source.Path != "" {
|
2026-11-10 20:17:54 +00:00
|
|
|
base := filepath.Dir(source.Path)
|
|
|
|
|
root := base
|
2026-11-10 16:13:53 +00:00
|
|
|
if context.BaseDir != "." {
|
|
|
|
|
for range strings.Split(context.BaseDir, "/") {
|
2026-11-10 20:17:54 +00:00
|
|
|
root = filepath.Dir(root)
|
2026-11-10 16:13:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-11-10 20:17:54 +00:00
|
|
|
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}
|
2026-11-10 16:13:53 +00:00
|
|
|
}
|
2026-11-10 20:17:54 +00:00
|
|
|
sources.Underlays = append(sources.Underlays, ck.Underlay{Inputs: []ck.Input{{
|
|
|
|
|
Source: ck.Bytes("Audita automatic prompt imports", []byte(operatorImports)), Imports: true,
|
|
|
|
|
Filesystem: &filesystemContext,
|
|
|
|
|
}}})
|
2026-11-10 16:13:53 +00:00
|
|
|
break
|
2026-10-04 00:33:31 +00:00
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
engine, err := pk.NewEngine(pk.Config{Sources: sources, HTTPClient: client}, pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
2026-09-24 01:50:10 +00:00
|
|
|
if err != nil {
|
2026-11-10 16:13:53 +00:00
|
|
|
return nil, embeddedAssetOrInputError(ctx, embedded, fmt.Errorf("assemble LLM runtime: %w", err))
|
2026-09-24 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
for _, meta := range prompts.RegisteredMetadata() {
|
2026-11-10 16:13:53 +00:00
|
|
|
if _, err := engine.InspectPrompt(ctx, meta.PromptID); err != nil {
|
|
|
|
|
return nil, embeddedAssetOrInputError(ctx, embedded, fmt.Errorf("inspect prompt %s: %w", meta.PromptID, err))
|
2026-09-24 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-11-10 20:35:35 +00:00
|
|
|
return engine, nil
|
2026-09-24 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-11-10 16:13:53 +00:00
|
|
|
func embeddedAssetOrInputError(ctx context.Context, inputs []ck.Input, cause error) error {
|
2026-10-04 00:45:23 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
engine, err := pk.NewEngine(pk.Config{Sources: pk.ConfigurationSources{Underlays: []ck.Underlay{{Inputs: inputs}}}}, pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
2026-10-04 00:45:23 +00:00
|
|
|
if err == nil {
|
|
|
|
|
for _, meta := range prompts.RegisteredMetadata() {
|
2026-11-10 16:13:53 +00:00
|
|
|
if _, err = engine.InspectPrompt(ctx, meta.PromptID); err != nil {
|
2026-10-04 00:45:23 +00:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-10 16:13:53 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return err
|
2026-10-04 00:45:23 +00:00
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return &AssetError{Err: fmt.Errorf("validate embedded prompt assets: %w", err)}
|
|
|
|
|
}
|
|
|
|
|
return cause
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 01:50:10 +00:00
|
|
|
func IsAssetError(err error) bool { var target *AssetError; return errors.As(err, &target) }
|