218 lines
6.8 KiB
Go
218 lines
6.8 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
const providerOpenAICompatible = "openai-compatible"
|
|
|
|
func (c Config) Validate() error {
|
|
if err := validateLLMProfiles(c.LLMProfiles); err != nil {
|
|
return err
|
|
}
|
|
if err := validateDiagnostics(c.Diagnostics); err != nil {
|
|
return err
|
|
}
|
|
if c.Concurrency.TotalLLM <= 0 {
|
|
return fmt.Errorf("total LLM concurrency must be greater than zero")
|
|
}
|
|
return validatePipelineProfiles(c.Pipelines, c.LLMProfiles)
|
|
}
|
|
|
|
func (c Config) LLMProfile(id string) (LLMProfile, bool) {
|
|
trimmedID := strings.TrimSpace(id)
|
|
for rawID, profile := range c.LLMProfiles {
|
|
if strings.TrimSpace(rawID) == trimmedID {
|
|
return profile, true
|
|
}
|
|
}
|
|
return LLMProfile{}, false
|
|
}
|
|
|
|
func validateLLMProfiles(profiles map[string]LLMProfile) error {
|
|
seen := make(map[string]struct{}, len(profiles))
|
|
for rawID, profile := range profiles {
|
|
id := strings.TrimSpace(rawID)
|
|
if id == "" {
|
|
return fmt.Errorf("LLM profile id must not be empty")
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
return fmt.Errorf("LLM profile id %q is duplicated after trimming", id)
|
|
}
|
|
seen[id] = struct{}{}
|
|
|
|
provider := strings.TrimSpace(profile.Provider)
|
|
if provider != "" && provider != providerOpenAICompatible {
|
|
return fmt.Errorf("LLM profile %q provider %q is not supported", id, provider)
|
|
}
|
|
if profile.TimeoutSeconds < 0 {
|
|
return fmt.Errorf("LLM profile %q timeout seconds must not be negative", id)
|
|
}
|
|
if profile.MaxRetries < 0 {
|
|
return fmt.Errorf("LLM profile %q max retries must not be negative", id)
|
|
}
|
|
if profile.MaxConcurrency < 0 {
|
|
return fmt.Errorf("LLM profile %q max concurrency must not be negative", id)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateDiagnostics(cfg DiagnosticsConfig) error {
|
|
if strings.TrimSpace(cfg.WorkDir) == "" {
|
|
return fmt.Errorf("diagnostics work dir must not be empty")
|
|
}
|
|
switch cfg.Retention {
|
|
case "", diagnostics.RetentionAuto, diagnostics.RetentionAlways, diagnostics.RetentionNever:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("diagnostics retention %q is not supported", cfg.Retention)
|
|
}
|
|
}
|
|
|
|
func validatePipelineProfiles(profiles map[string]pipeline.PipelineProfile, llmProfiles map[string]LLMProfile) error {
|
|
seen := make(map[string]struct{}, len(profiles))
|
|
for rawID, profile := range profiles {
|
|
id := strings.TrimSpace(rawID)
|
|
if id == "" {
|
|
return fmt.Errorf("pipeline id must not be empty")
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
return fmt.Errorf("pipeline id %q is duplicated after trimming", id)
|
|
}
|
|
seen[id] = struct{}{}
|
|
|
|
if profile.ID != "" && strings.TrimSpace(profile.ID) != id {
|
|
return fmt.Errorf("pipeline %q profile id %q does not match map key", id, profile.ID)
|
|
}
|
|
if err := validateBinding(id, "", "input", profile.Input, llmProfiles, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, "", "chunk", profile.Chunk, llmProfiles, true); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, "", "output", profile.Output, llmProfiles, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateReferenceMap(id, "", profile.References); err != nil {
|
|
return err
|
|
}
|
|
for rawLaneID, lane := range profile.Artifacts {
|
|
laneID := strings.TrimSpace(rawLaneID)
|
|
if laneID == "" {
|
|
return fmt.Errorf("pipeline %q artifact lane id must not be empty", id)
|
|
}
|
|
if err := validateReferenceMap(id, laneID, lane.References); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, laneID, "extract", lane.Extract, llmProfiles, true); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, laneID, "merge", lane.Merge, llmProfiles, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, laneID, "normalize", lane.Normalize, llmProfiles, true); err != nil {
|
|
return err
|
|
}
|
|
for i, validator := range lane.Validators {
|
|
if err := validateBinding(id, laneID, fmt.Sprintf("validator[%d]", i), validator, llmProfiles, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateBinding(
|
|
pipelineID string,
|
|
laneID string,
|
|
slot string,
|
|
binding pipeline.ModuleBinding,
|
|
profiles map[string]LLMProfile,
|
|
referencesAllowed bool,
|
|
) error {
|
|
if err := validateBindingLLMProfile(pipelineID, laneID, slot, binding, profiles); err != nil {
|
|
return err
|
|
}
|
|
if len(binding.References) == 0 {
|
|
return nil
|
|
}
|
|
if !referencesAllowed {
|
|
if laneID != "" {
|
|
return fmt.Errorf("pipeline %q lane %q %s references are not supported", pipelineID, laneID, slot)
|
|
}
|
|
return fmt.Errorf("pipeline %q %s references are not supported", pipelineID, slot)
|
|
}
|
|
return validateReferenceMapForContext(pipelineID, laneID, slot, binding.References)
|
|
}
|
|
|
|
func validateReferenceMap(pipelineID string, laneID string, references map[string]string) error {
|
|
return validateReferenceMapForContext(pipelineID, laneID, "", references)
|
|
}
|
|
|
|
func validateReferenceMapForContext(pipelineID string, laneID string, slot string, references map[string]string) error {
|
|
seen := make(map[string]struct{}, len(references))
|
|
for rawSlotName, rawSource := range references {
|
|
slotName := strings.TrimSpace(rawSlotName)
|
|
if slotName == "" {
|
|
return fmt.Errorf("%s reference slot name must not be empty", referenceContext(pipelineID, laneID, slot))
|
|
}
|
|
if _, ok := seen[slotName]; ok {
|
|
return fmt.Errorf("%s reference slot %q is duplicated after trimming", referenceContext(pipelineID, laneID, slot), slotName)
|
|
}
|
|
seen[slotName] = struct{}{}
|
|
if strings.TrimSpace(rawSource) == "" {
|
|
return fmt.Errorf("%s reference slot %q source must not be empty", referenceContext(pipelineID, laneID, slot), slotName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func referenceContext(pipelineID string, laneID string, slot string) string {
|
|
if laneID != "" && slot != "" {
|
|
return fmt.Sprintf("pipeline %q lane %q %s", pipelineID, laneID, slot)
|
|
}
|
|
if laneID != "" {
|
|
return fmt.Sprintf("pipeline %q lane %q", pipelineID, laneID)
|
|
}
|
|
if slot != "" {
|
|
return fmt.Sprintf("pipeline %q %s", pipelineID, slot)
|
|
}
|
|
return fmt.Sprintf("pipeline %q", pipelineID)
|
|
}
|
|
|
|
func validateBindingLLMProfile(
|
|
pipelineID string,
|
|
laneID string,
|
|
slot string,
|
|
binding pipeline.ModuleBinding,
|
|
profiles map[string]LLMProfile,
|
|
) error {
|
|
profileID := strings.TrimSpace(binding.LLMProfile)
|
|
if profileID == "" {
|
|
profileID = pipeline.DefaultLLMProfile
|
|
}
|
|
if hasLLMProfile(profiles, profileID) {
|
|
return nil
|
|
}
|
|
if laneID != "" {
|
|
return fmt.Errorf("pipeline %q lane %q %s references unknown LLM profile %q", pipelineID, laneID, slot, profileID)
|
|
}
|
|
return fmt.Errorf("pipeline %q %s references unknown LLM profile %q", pipelineID, slot, profileID)
|
|
}
|
|
|
|
func hasLLMProfile(profiles map[string]LLMProfile, profileID string) bool {
|
|
profileID = strings.TrimSpace(profileID)
|
|
for rawID := range profiles {
|
|
if strings.TrimSpace(rawID) == profileID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|