115 lines
3.0 KiB
Go
115 lines
3.0 KiB
Go
// Package ai handles immutable Agent-version snapshots. The JSON Schema is
|
|
// loaded from the pinned upstream contract bundle; this package does not
|
|
// duplicate or loosen that schema.
|
|
package ai
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.ipao.vip/rogee/go-sip/contracts"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
)
|
|
|
|
type Mode string
|
|
|
|
const (
|
|
ModeFullAI Mode = "full_ai"
|
|
ModeASROnly Mode = "asr_only"
|
|
)
|
|
|
|
type Snapshot struct {
|
|
TenantKey string
|
|
AgentVersionID string
|
|
Digest string
|
|
Raw []byte
|
|
Mode Mode
|
|
}
|
|
|
|
func Validate(raw []byte) (Snapshot, error) {
|
|
if err := contract.ValidateSourceSchema("ai-config.schema.json", raw); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
var value struct {
|
|
AgentVersionID string `json:"agent_version_id"`
|
|
Mode Mode `json:"mode"`
|
|
}
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
mode := value.Mode
|
|
if mode == "" {
|
|
// Legacy immutable snapshots predate the explicit mode field and are
|
|
// full-AI snapshots because llm/prompt/tts are required by that branch.
|
|
mode = ModeFullAI
|
|
}
|
|
if mode != ModeFullAI && mode != ModeASROnly {
|
|
return Snapshot{}, fmt.Errorf("unsupported AI mode %q", mode)
|
|
}
|
|
digest := sha256.Sum256(raw)
|
|
return Snapshot{AgentVersionID: value.AgentVersionID, Digest: hex.EncodeToString(digest[:]), Raw: append([]byte(nil), raw...), Mode: mode}, nil
|
|
}
|
|
|
|
func ValidateForMode(raw []byte, mode Mode) (Snapshot, error) {
|
|
if mode != ModeFullAI && mode != ModeASROnly {
|
|
return Snapshot{}, fmt.Errorf("unsupported AI mode %q", mode)
|
|
}
|
|
snapshot, err := Validate(raw)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
if snapshot.Mode != mode {
|
|
return Snapshot{}, fmt.Errorf("AI config mode %q does not match requested mode %q", snapshot.Mode, mode)
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func EnsureSameVersion(previous, next Snapshot) error {
|
|
if previous.AgentVersionID == "" || next.AgentVersionID == "" || previous.AgentVersionID != next.AgentVersionID {
|
|
return errors.New("agent version identity changed")
|
|
}
|
|
if previous.Digest != next.Digest {
|
|
return errors.New("immutable agent version content changed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Cache struct {
|
|
mu sync.RWMutex
|
|
items map[string]Snapshot
|
|
}
|
|
|
|
func NewCache() *Cache { return &Cache{items: make(map[string]Snapshot)} }
|
|
|
|
func (c *Cache) Put(tenantKey string, snapshot Snapshot) error {
|
|
if err := contract.ValidateTenantKey(tenantKey); err != nil {
|
|
return err
|
|
}
|
|
if snapshot.AgentVersionID == "" || snapshot.Digest == "" {
|
|
return errors.New("snapshot identity is required")
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
key := tenantKey + "\x00" + snapshot.AgentVersionID
|
|
if old, ok := c.items[key]; ok {
|
|
if err := EnsureSameVersion(old, snapshot); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c.items[key] = snapshot
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) Get(tenantKey, versionID string) (Snapshot, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
s, ok := c.items[tenantKey+"\x00"+versionID]
|
|
return s, ok
|
|
}
|
|
|
|
func ContractSource() string { return contracts.SourceCommit }
|