Assemble PromptKit profiles for offline configuration inspection
This commit is contained in:
108
internal/framework/llm/runtime.go
Normal file
108
internal/framework/llm/runtime.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
profileDir := c.ProfilesDir
|
||||
if !filepath.IsAbs(profileDir) {
|
||||
profileDir = filepath.Join(filepath.Dir(mainPath), profileDir)
|
||||
}
|
||||
options := []pk.Option{
|
||||
pk.WithPromptFS(prompts.Filesystem(), prompts.AssetRoot),
|
||||
pk.WithSchemaFS(responseschema.Filesystem(), responseschema.AssetRoot),
|
||||
pk.WithProfileFS(os.DirFS(profileDir), "."),
|
||||
}
|
||||
ids := make([]string, 0, len(c.Backends))
|
||||
for id := range c.Backends {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
for _, id := range ids {
|
||||
backend := c.Backends[id]
|
||||
options = append(options, pk.WithBackend(pk.Backend{ID: id, Endpoint: backend.Endpoint, ConcurrencyLimit: backend.Concurrency, QueueCapacity: &backend.QueueCapacity}))
|
||||
}
|
||||
engine, err := pk.NewEngine(pk.Config{HTTPClient: client, TransientRetries: &c.LLM.TransientRetries}, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("assemble LLM runtime: %w", err)
|
||||
}
|
||||
for _, meta := range prompts.RegisteredMetadata() {
|
||||
if _, err := engine.InspectPrompt(ctx, meta.PromptID, meta.PromptVersion); err != nil {
|
||||
return nil, &AssetError{Err: fmt.Errorf("inspect embedded prompt %s: %w", meta.PromptID, err)}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
return runtime, nil
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
message := err.Error()
|
||||
var secrets []string
|
||||
for _, backend := range c.Backends {
|
||||
if backend.Credential != nil && backend.Credential.Value() != "" {
|
||||
secrets = append(secrets, backend.Credential.Value())
|
||||
}
|
||||
}
|
||||
sort.Slice(secrets, func(i, j int) bool { return len(secrets[i]) > len(secrets[j]) })
|
||||
for _, secret := range secrets {
|
||||
message = strings.ReplaceAll(message, secret, "[REDACTED]")
|
||||
}
|
||||
return message
|
||||
}
|
||||
Reference in New Issue
Block a user