172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
package llm
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"sort"
|
|
"strings"
|
|
|
|
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
|
|
)
|
|
|
|
var schemaAssets = mustSubFS(rootassets.FS(), "generic")
|
|
|
|
// ResponseSchemaKey identifies one structured response schema.
|
|
type ResponseSchemaKey string
|
|
|
|
const (
|
|
TestArtifactSchemaKey ResponseSchemaKey = "test_artifact"
|
|
TestValidatorDecisionSchemaKey ResponseSchemaKey = "test_validator_decision"
|
|
|
|
schemaVersionV1 = "v1"
|
|
)
|
|
|
|
// ResponseSchemaDefinition identifies a caller-owned structured response schema asset.
|
|
type ResponseSchemaDefinition struct {
|
|
Key ResponseSchemaKey
|
|
ID string
|
|
Version string
|
|
Name string
|
|
AssetPath string
|
|
}
|
|
|
|
// ResponseSchema describes one registered structured response schema.
|
|
type ResponseSchema struct {
|
|
Key ResponseSchemaKey `json:"key"`
|
|
ID string `json:"id"`
|
|
Version string `json:"version"`
|
|
Name string `json:"name"`
|
|
JSONSchema json.RawMessage `json:"json_schema"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
|
|
var responseSchemaRegistry = map[ResponseSchemaKey]ResponseSchema{
|
|
TestArtifactSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{
|
|
Key: TestArtifactSchemaKey,
|
|
ID: "notarius.test_artifact",
|
|
Version: schemaVersionV1,
|
|
Name: "notarius_test_artifact_v1",
|
|
AssetPath: "schemas/test_artifact.v1.json",
|
|
}),
|
|
TestValidatorDecisionSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{
|
|
Key: TestValidatorDecisionSchemaKey,
|
|
ID: "notarius.test_validator_decision",
|
|
Version: schemaVersionV1,
|
|
Name: "notarius_test_validator_decision_v1",
|
|
AssetPath: "schemas/test_validator_decision.v1.json",
|
|
}),
|
|
}
|
|
|
|
func mustSubFS(fsys fs.FS, dir string) fs.FS {
|
|
sub, err := fs.Sub(fsys, dir)
|
|
if err != nil {
|
|
panic(fmt.Errorf("scope embedded assets to %s: %w", dir, err))
|
|
}
|
|
return sub
|
|
}
|
|
|
|
// RegisteredResponseSchemas returns all registered response schemas sorted by key.
|
|
func RegisteredResponseSchemas() []ResponseSchema {
|
|
keys := make([]string, 0, len(responseSchemaRegistry))
|
|
for key := range responseSchemaRegistry {
|
|
keys = append(keys, string(key))
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
out := make([]ResponseSchema, 0, len(keys))
|
|
for _, key := range keys {
|
|
out = append(out, cloneResponseSchema(responseSchemaRegistry[ResponseSchemaKey(key)]))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// LookupResponseSchema returns a copy of the schema for key.
|
|
func LookupResponseSchema(key ResponseSchemaKey) (ResponseSchema, bool) {
|
|
schema, ok := responseSchemaRegistry[key]
|
|
if !ok {
|
|
return ResponseSchema{}, false
|
|
}
|
|
return cloneResponseSchema(schema), true
|
|
}
|
|
|
|
// MustLookupResponseSchema returns a copy of the schema for key and panics when missing.
|
|
func MustLookupResponseSchema(key ResponseSchemaKey) ResponseSchema {
|
|
schema, ok := LookupResponseSchema(key)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown structured response schema key %q", key))
|
|
}
|
|
return schema
|
|
}
|
|
|
|
// DiagnosticsMap returns schema metadata without raw schema content.
|
|
func (s ResponseSchema) DiagnosticsMap() map[string]any {
|
|
return map[string]any{
|
|
"key": s.Key,
|
|
"id": s.ID,
|
|
"version": s.Version,
|
|
"name": s.Name,
|
|
"sha256": s.SHA256,
|
|
}
|
|
}
|
|
|
|
// LoadResponseSchema loads a structured response schema from a caller-owned filesystem.
|
|
func LoadResponseSchema(fsys fs.FS, def ResponseSchemaDefinition) (ResponseSchema, error) {
|
|
key := ResponseSchemaKey(strings.TrimSpace(string(def.Key)))
|
|
id := strings.TrimSpace(def.ID)
|
|
version := strings.TrimSpace(def.Version)
|
|
name := strings.TrimSpace(def.Name)
|
|
path := strings.TrimSpace(def.AssetPath)
|
|
if key == "" {
|
|
return ResponseSchema{}, fmt.Errorf("response schema key must not be empty")
|
|
}
|
|
if id == "" {
|
|
return ResponseSchema{}, fmt.Errorf("response schema id must not be empty")
|
|
}
|
|
if version == "" {
|
|
return ResponseSchema{}, fmt.Errorf("response schema version must not be empty")
|
|
}
|
|
if name == "" {
|
|
return ResponseSchema{}, fmt.Errorf("response schema name must not be empty")
|
|
}
|
|
if path == "" {
|
|
return ResponseSchema{}, fmt.Errorf("response schema asset path must not be empty")
|
|
}
|
|
|
|
rawSchema, err := fs.ReadFile(fsys, path)
|
|
if err != nil {
|
|
return ResponseSchema{}, fmt.Errorf("read response schema %s: %w", path, err)
|
|
}
|
|
if !json.Valid(rawSchema) {
|
|
return ResponseSchema{}, fmt.Errorf("response schema %s is not valid JSON", path)
|
|
}
|
|
|
|
hash := sha256.Sum256(rawSchema)
|
|
return ResponseSchema{
|
|
Key: key,
|
|
ID: id,
|
|
Version: version,
|
|
Name: name,
|
|
JSONSchema: append(json.RawMessage(nil), rawSchema...),
|
|
SHA256: "sha256:" + hex.EncodeToString(hash[:]),
|
|
}, nil
|
|
}
|
|
|
|
func mustLoadResponseSchema(fsys fs.FS, def ResponseSchemaDefinition) ResponseSchema {
|
|
schema, err := LoadResponseSchema(fsys, def)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func cloneResponseSchema(in ResponseSchema) ResponseSchema {
|
|
out := in
|
|
if in.JSONSchema != nil {
|
|
out.JSONSchema = append(json.RawMessage(nil), in.JSONSchema...)
|
|
}
|
|
return out
|
|
}
|