296 lines
11 KiB
Go
296 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
func resolveDocumentOperations(document *Document, resolution *configkit.Resolution, state *documentLoadState) error {
|
|
if document == nil {
|
|
return nil
|
|
}
|
|
normalizeDocumentPromptKit(document.PromptKit, resolution)
|
|
normalizeDocumentState(document, resolution)
|
|
normalizeDocumentPipelines(document, resolution, state)
|
|
normalizeDocumentPaths(document, resolution, state)
|
|
deriveDocumentExtractWorkers(document, resolution)
|
|
return nil
|
|
}
|
|
|
|
func normalizeDocumentPromptKit(promptKit *PromptKitDocument, resolution *configkit.Resolution) {
|
|
if promptKit == nil {
|
|
return
|
|
}
|
|
profileDirPath := configkit.Keys("promptkit", "profile_dir")
|
|
profileFilePath := configkit.Keys("promptkit", "profile_file")
|
|
if promptKit.ProfileDir == nil && resolution.IsExplicit(profileDirPath) {
|
|
resolution.Error(profileDirPath, "must not be null")
|
|
}
|
|
if promptKit.ProfileFile == nil && resolution.IsExplicit(profileFilePath) {
|
|
resolution.Error(profileFilePath, "must not be null")
|
|
}
|
|
normalizeDocumentString(resolution, profileDirPath, &promptKit.ProfileDir, "trimmed PromptKit profile directory")
|
|
normalizeDocumentString(resolution, profileFilePath, &promptKit.ProfileFile, "trimmed PromptKit profile file")
|
|
|
|
if promptKit.LocalBackend == nil {
|
|
return
|
|
}
|
|
endpointPath := configkit.Keys("promptkit", "local_backend", "endpoint")
|
|
limitPath := configkit.Keys("promptkit", "local_backend", "concurrency_limit")
|
|
normalizeDocumentString(resolution, endpointPath, &promptKit.LocalBackend.Endpoint, "trimmed PromptKit local backend endpoint")
|
|
if promptKit.LocalBackend.ConcurrencyLimit == nil && !resolution.IsExplicit(limitPath) {
|
|
configkit.Default(resolution, limitPath, &promptKit.LocalBackend.ConcurrencyLimit,
|
|
documentValue(0), "defaulted PromptKit local backend concurrency limit")
|
|
}
|
|
}
|
|
|
|
func normalizeDocumentState(document *Document, resolution *configkit.Resolution) {
|
|
if document.Output != nil {
|
|
normalizeDocumentString(resolution, configkit.Keys("output", "directory"), &document.Output.Directory, "trimmed output directory")
|
|
}
|
|
if document.Cache != nil {
|
|
if document.Cache.ChunkPlans != nil {
|
|
normalizeDocumentString(resolution, configkit.Keys("cache", "chunk_plans", "directory"), &document.Cache.ChunkPlans.Directory, "trimmed chunk-plan cache directory")
|
|
normalizeDocumentString(resolution, configkit.Keys("cache", "chunk_plans", "mode"), &document.Cache.ChunkPlans.Mode, "trimmed chunk-plan cache mode")
|
|
}
|
|
if document.Cache.Checkpoints != nil {
|
|
normalizeDocumentString(resolution, configkit.Keys("cache", "checkpoints", "directory"), &document.Cache.Checkpoints.Directory, "trimmed checkpoint cache directory")
|
|
}
|
|
}
|
|
if document.Debug != nil {
|
|
normalizeDocumentString(resolution, configkit.Keys("debug", "directory"), &document.Debug.Directory, "trimmed debug directory")
|
|
}
|
|
}
|
|
|
|
func normalizeDocumentString(resolution *configkit.Resolution, path configkit.Path, target **string, reason string) {
|
|
if target == nil || *target == nil || !resolution.IsExplicit(path) {
|
|
return
|
|
}
|
|
configkit.Normalize(resolution, path, target, documentValue(strings.TrimSpace(**target)), reason)
|
|
}
|
|
|
|
func deriveDocumentExtractWorkers(document *Document, resolution *configkit.Resolution) {
|
|
if document.Concurrency == nil || document.Concurrency.TotalLLM == nil {
|
|
return
|
|
}
|
|
if document.Concurrency.StageWorkers == nil {
|
|
document.Concurrency.StageWorkers = make(map[string]int)
|
|
}
|
|
path := configkit.Keys("concurrency", "stage_workers", "extract")
|
|
workers := document.Concurrency.StageWorkers
|
|
value := workers["extract"]
|
|
configkit.Default(resolution, path, &value, *document.Concurrency.TotalLLM,
|
|
"derived extraction workers from total LLM concurrency", configkit.Keys("concurrency", "total_llm"))
|
|
workers["extract"] = value
|
|
}
|
|
|
|
func validateDocumentOperations(document *Document, validation *configkit.Validation) {
|
|
if document == nil {
|
|
return
|
|
}
|
|
validateDocumentConcurrency(document.Concurrency, validation)
|
|
validateDocumentPromptKit(document.PromptKit, validation)
|
|
validateDocumentState(document, validation)
|
|
validateDocumentPipelines(document, validation)
|
|
}
|
|
|
|
func validateDocumentConcurrency(concurrency *ConcurrencyDocument, validation *configkit.Validation) {
|
|
if concurrency == nil {
|
|
validation.Error(configkit.Keys("concurrency"), "must not be null")
|
|
return
|
|
}
|
|
totalPath := configkit.Keys("concurrency", "total_llm")
|
|
if concurrency.TotalLLM == nil {
|
|
validation.Error(totalPath, "must not be null")
|
|
return
|
|
}
|
|
totalValid := true
|
|
if err := validateTotalLLMConcurrency(*concurrency.TotalLLM); err != nil {
|
|
validation.Error(totalPath, err.Error())
|
|
totalValid = false
|
|
}
|
|
keys := make([]string, 0, len(concurrency.StageWorkers))
|
|
for key := range concurrency.StageWorkers {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
if err := validateStageWorkerKey(key); err != nil {
|
|
validation.Error(configkit.Keys("concurrency", "stage_workers").Key(key), err.Error())
|
|
}
|
|
}
|
|
if !totalValid {
|
|
return
|
|
}
|
|
if workers, ok := concurrency.StageWorkers["extract"]; ok {
|
|
if err := validateExtractWorkerCount(workers, *concurrency.TotalLLM); err != nil {
|
|
validation.Error(configkit.Keys("concurrency", "stage_workers", "extract"), err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func validateDocumentPromptKit(promptKit *PromptKitDocument, validation *configkit.Validation) {
|
|
if promptKit == nil {
|
|
return
|
|
}
|
|
profileDirPath := configkit.Keys("promptkit", "profile_dir")
|
|
profileFilePath := configkit.Keys("promptkit", "profile_file")
|
|
profileDir := ""
|
|
if promptKit.ProfileDir != nil {
|
|
profileDir = *promptKit.ProfileDir
|
|
if strings.TrimSpace(profileDir) == "" {
|
|
validation.Error(profileDirPath, "must not be empty when set")
|
|
}
|
|
}
|
|
profileFile := ""
|
|
if promptKit.ProfileFile != nil {
|
|
profileFile = *promptKit.ProfileFile
|
|
if strings.TrimSpace(profileFile) == "" {
|
|
validation.Error(profileFilePath, "must not be empty when set")
|
|
}
|
|
}
|
|
if err := validatePromptKitProfileSources(profileDir, profileFile); err != nil {
|
|
validation.Error(configkit.Keys("promptkit"), err.Error())
|
|
}
|
|
if promptKit.LocalBackend == nil {
|
|
return
|
|
}
|
|
endpointPath := configkit.Keys("promptkit", "local_backend", "endpoint")
|
|
limitPath := configkit.Keys("promptkit", "local_backend", "concurrency_limit")
|
|
if promptKit.LocalBackend.Endpoint == nil {
|
|
validation.Error(endpointPath, "must be nonempty when local backend is configured")
|
|
}
|
|
if promptKit.LocalBackend.ConcurrencyLimit == nil {
|
|
validation.Error(limitPath, "must be an integer when local backend is configured")
|
|
}
|
|
if promptKit.LocalBackend.Endpoint != nil {
|
|
if err := validatePromptKitLocalBackendEndpoint(*promptKit.LocalBackend.Endpoint); err != nil {
|
|
validation.Error(endpointPath, err.Error())
|
|
}
|
|
}
|
|
if promptKit.LocalBackend.ConcurrencyLimit != nil {
|
|
if err := validatePromptKitLocalBackendConcurrencyLimit(*promptKit.LocalBackend.ConcurrencyLimit); err != nil {
|
|
validation.Error(limitPath, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func validateDocumentState(document *Document, validation *configkit.Validation) {
|
|
if document.Output == nil || document.Output.Directory == nil {
|
|
validation.Error(configkit.Keys("output", "directory"), "must not be empty")
|
|
} else if err := validateRequiredPath("output.directory", *document.Output.Directory); err != nil {
|
|
validation.Error(configkit.Keys("output", "directory"), err.Error())
|
|
}
|
|
|
|
if document.Cache == nil || document.Cache.ChunkPlans == nil || document.Cache.ChunkPlans.Mode == nil {
|
|
validation.Error(configkit.Keys("cache", "chunk_plans", "mode"), "must be a supported cache mode")
|
|
} else if err := validateChunkPlanCacheMode(*document.Cache.ChunkPlans.Mode); err != nil {
|
|
validation.Error(configkit.Keys("cache", "chunk_plans", "mode"), err.Error())
|
|
}
|
|
if document.Cache != nil && document.Cache.ChunkPlans != nil && document.Cache.ChunkPlans.Directory != nil {
|
|
if err := validateOptionalPath("cache.chunk_plans.directory", *document.Cache.ChunkPlans.Directory); err != nil {
|
|
validation.Error(configkit.Keys("cache", "chunk_plans", "directory"), err.Error())
|
|
}
|
|
}
|
|
if document.Cache != nil && document.Cache.Checkpoints != nil && document.Cache.Checkpoints.Directory != nil {
|
|
if err := validateOptionalPath("cache.checkpoints.directory", *document.Cache.Checkpoints.Directory); err != nil {
|
|
validation.Error(configkit.Keys("cache", "checkpoints", "directory"), err.Error())
|
|
}
|
|
}
|
|
|
|
if document.Debug == nil || document.Debug.Directory == nil {
|
|
validation.Error(configkit.Keys("debug", "directory"), "must not be empty")
|
|
} else if err := validateRequiredPath("debug.directory", *document.Debug.Directory); err != nil {
|
|
validation.Error(configkit.Keys("debug", "directory"), err.Error())
|
|
}
|
|
}
|
|
|
|
func validateTotalLLMConcurrency(total int) error {
|
|
if total <= 0 {
|
|
return fmt.Errorf("total LLM concurrency must be greater than zero")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateStageWorkerKey(key string) error {
|
|
if strings.TrimSpace(key) == "" {
|
|
return fmt.Errorf("concurrency.stage_workers key must not be empty")
|
|
}
|
|
if key != "extract" {
|
|
return fmt.Errorf("concurrency.stage_workers key %q is not supported", key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateExtractWorkerCount(workers, total int) error {
|
|
if workers < 1 || workers > total {
|
|
return fmt.Errorf("concurrency.stage_workers.extract must be between 1 and concurrency.total_llm (%d)", total)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePromptKitProfileSources(profileDir, profileFile string) error {
|
|
if strings.TrimSpace(profileDir) != "" && strings.TrimSpace(profileFile) != "" {
|
|
return fmt.Errorf("promptkit profile_dir and profile_file are mutually exclusive")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePromptKitLocalBackend(endpoint string, concurrencyLimit int) error {
|
|
if err := validatePromptKitLocalBackendEndpoint(endpoint); err != nil {
|
|
return err
|
|
}
|
|
return validatePromptKitLocalBackendConcurrencyLimit(concurrencyLimit)
|
|
}
|
|
|
|
func validatePromptKitLocalBackendEndpoint(endpoint string) error {
|
|
if strings.TrimSpace(endpoint) == "" {
|
|
return fmt.Errorf("promptkit.local_backend.endpoint must not be empty when set")
|
|
}
|
|
parsed, err := url.Parse(strings.TrimSpace(endpoint))
|
|
if err != nil ||
|
|
(!strings.EqualFold(parsed.Scheme, "http") && !strings.EqualFold(parsed.Scheme, "https")) ||
|
|
!parsed.IsAbs() ||
|
|
parsed.Hostname() == "" ||
|
|
parsed.User != nil ||
|
|
parsed.RawQuery != "" ||
|
|
parsed.ForceQuery ||
|
|
strings.Contains(endpoint, "#") {
|
|
return fmt.Errorf("promptkit.local_backend.endpoint must be an absolute HTTP or HTTPS URL with a host and no user information, query, or fragment")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePromptKitLocalBackendConcurrencyLimit(concurrencyLimit int) error {
|
|
if concurrencyLimit < 0 {
|
|
return fmt.Errorf("promptkit.local_backend.concurrency_limit must not be negative")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRequiredPath(name, value string) error {
|
|
if strings.TrimSpace(value) == "" {
|
|
return fmt.Errorf("%s must not be empty", name)
|
|
}
|
|
return validateOptionalPath(name, value)
|
|
}
|
|
|
|
func validateOptionalPath(name, value string) error {
|
|
if strings.ContainsRune(value, '\x00') {
|
|
return fmt.Errorf("%s must not contain NUL", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateChunkPlanCacheMode(value string) error {
|
|
if _, err := pipeline.ParseChunkCacheMode(value); err != nil {
|
|
return fmt.Errorf("cache.chunk_plans.mode: %w", err)
|
|
}
|
|
return nil
|
|
}
|