171 lines
6.1 KiB
Go
171 lines
6.1 KiB
Go
package llm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
ck "gitea.maximumdirect.net/eric/ConfigKit"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/redaction"
|
|
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
|
|
"gitea.maximumdirect.net/eric/audita/internal/prompts"
|
|
pk "gitea.maximumdirect.net/eric/promptkit"
|
|
)
|
|
|
|
// Runtime owns one PromptKit engine and the validated selections for both roles.
|
|
// The same engine is shared by the role-bound clients during processing.
|
|
type Runtime struct {
|
|
Config config.Config
|
|
Engine *pk.Engine
|
|
Profiles map[string]pk.ProfileInspection
|
|
// SourcePaths contains host configuration and message files captured by PromptKit.
|
|
SourcePaths []string
|
|
nextCallID atomic.Uint64
|
|
}
|
|
|
|
// AssetError indicates a broken embedded prompt or schema rather than an
|
|
// operator-provided setting or profile.
|
|
type AssetError struct{ Err error }
|
|
|
|
func (e *AssetError) Error() string { return e.Err.Error() }
|
|
func (e *AssetError) Unwrap() error { return e.Err }
|
|
|
|
func NewRuntime(ctx context.Context, c config.Config, mainPath string, client *http.Client) (*Runtime, error) {
|
|
return newRuntimeWithPromptFS(ctx, c, mainPath, client, prompts.Filesystem())
|
|
}
|
|
|
|
func newRuntimeWithPromptFS(ctx context.Context, c config.Config, mainPath string, client *http.Client, promptFS fs.FS) (*Runtime, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
profileDir := c.ProfilesDir
|
|
if !filepath.IsAbs(profileDir) {
|
|
profileDir = filepath.Join(filepath.Dir(mainPath), profileDir)
|
|
}
|
|
promptInputs, err := pk.FilesystemInputs("Audita embedded prompts", promptFS, prompts.AssetRoot)
|
|
if err != nil {
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return nil, ctxErr
|
|
}
|
|
return nil, &AssetError{Err: fmt.Errorf("discover embedded prompts: %w", err)}
|
|
}
|
|
profileInputs, err := pk.DirectoryInputs(profileDir)
|
|
if err != nil {
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return nil, ctxErr
|
|
}
|
|
return nil, fmt.Errorf("discover operator definitions: %w", err)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
applicationBackends := make(map[string]any, len(c.Backends))
|
|
for id := range c.Backends {
|
|
backend := c.Backends[id]
|
|
applicationBackends[id] = map[string]any{
|
|
"endpoint": backend.Endpoint,
|
|
"concurrency_limit": backend.Concurrency,
|
|
"queue_capacity": backend.QueueCapacity,
|
|
"api_key_env": nil,
|
|
}
|
|
}
|
|
sources := pk.ConfigurationSources{
|
|
Underlays: []ck.Underlay{
|
|
{Inputs: promptInputs},
|
|
{Inputs: profileInputs},
|
|
},
|
|
Main: &ck.Input{Source: ck.Mapping("application backends", map[string]any{"backends": applicationBackends})},
|
|
}
|
|
engine, err := pk.NewEngine(pk.Config{
|
|
Sources: sources,
|
|
HTTPClient: client,
|
|
TransientRetries: &c.LLM.TransientRetries,
|
|
}, pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
|
if err != nil {
|
|
return nil, embeddedAssetOrInputError(ctx, promptInputs, fmt.Errorf("assemble LLM runtime: %w", err))
|
|
}
|
|
for _, meta := range prompts.RegisteredMetadata() {
|
|
if _, err := engine.InspectPrompt(ctx, meta.PromptID, meta.PromptVersion); err != nil {
|
|
cause := fmt.Errorf("inspect prompt %s: %w", meta.PromptID, err)
|
|
return nil, embeddedAssetOrInputError(ctx, promptInputs, cause)
|
|
}
|
|
}
|
|
runtime := &Runtime{Config: c, Engine: engine, Profiles: make(map[string]pk.ProfileInspection, 2)}
|
|
for _, selection := range []struct{ role, id string }{{"proposal", c.LLM.ProposalProfile}, {"validation", c.LLM.ValidationProfile}} {
|
|
profile, err := engine.InspectProfile(ctx, selection.id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inspect %s profile: %w", selection.role, err)
|
|
}
|
|
target := profile.EffectiveModelParams
|
|
backend, ok := c.Backends[target.BackendID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s profile must select a registered application backend", selection.role)
|
|
}
|
|
if strings.TrimSpace(target.Model) == "" {
|
|
return nil, fmt.Errorf("%s profile must select a nonblank model", selection.role)
|
|
}
|
|
if strings.TrimSpace(target.Endpoint) == "" {
|
|
return nil, fmt.Errorf("%s profile must resolve to a nonblank endpoint", selection.role)
|
|
}
|
|
endpoint := func(value string) string { return strings.TrimRight(strings.TrimSpace(value), "/") }
|
|
if endpoint(target.Endpoint) != endpoint(backend.Endpoint) {
|
|
return nil, fmt.Errorf("%s profile endpoint must match its registered backend", selection.role)
|
|
}
|
|
if target.APIKeyEnv != "" {
|
|
return nil, fmt.Errorf("%s profile must use the backend credential, not its own environment lookup", selection.role)
|
|
}
|
|
if profile.APIKeyRequired && backend.Credential == nil {
|
|
return nil, fmt.Errorf("%s profile requires a backend credential", selection.role)
|
|
}
|
|
runtime.Profiles[selection.role] = *profile
|
|
}
|
|
runtime.SourcePaths, err = capturedSourcePaths(engine)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inspect captured LLM sources: %w", err)
|
|
}
|
|
return runtime, nil
|
|
}
|
|
|
|
func embeddedAssetOrInputError(ctx context.Context, promptInputs []ck.Input, cause error) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
embeddedSources := pk.ConfigurationSources{
|
|
Underlays: []ck.Underlay{{Inputs: promptInputs}},
|
|
}
|
|
diagnosticEngine, err := pk.NewEngine(pk.Config{Sources: embeddedSources},
|
|
pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot))
|
|
if err == nil {
|
|
for _, meta := range prompts.RegisteredMetadata() {
|
|
if _, inspectErr := diagnosticEngine.InspectPrompt(ctx, meta.PromptID, meta.PromptVersion); inspectErr != nil {
|
|
err = fmt.Errorf("inspect embedded prompt %s: %w", meta.PromptID, inspectErr)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return ctxErr
|
|
}
|
|
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) }
|
|
|
|
// RedactError replaces every configured resolved backend secret before a
|
|
// library error is surfaced in CLI output or an artifact.
|
|
func RedactError(err error, c config.Config) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
return redaction.New(ConfiguredSecrets(&c)).String(err.Error())
|
|
}
|