// Package loader loads persona definitions from YAML files using gopkg.in/yaml.v3. // A persona is the self-awareness/identity bound to a character and, per // DevDocs/design/01, it also carries the emotion state-machine parameters, the // affection curve, and a per-persona memory namespace (memory isolation). package loader import ( "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/emotion" ) // Default affection bounds and step, applied when a persona defines no // affection curve. const ( defaultAffectionMin = 0 defaultAffectionMax = 100 defaultAffectionInit = 50 defaultAffectionDelta = 5 ) // defaultMemoryNS is the namespace prefix used when a persona omits a concrete // memory namespace; the full namespace becomes prefix + "." + persona id. const defaultMemoryNS = "persona" // Identity is the persona's self-awareness: the 人设 (who she is, e.g. a // character role) plus the system prompt that carries that identity at runtime. type Identity struct { // Role is the 人设 — the characterization the persona assumes. Role string `yaml:"role"` // SystemPrompt is the identity block injected as the system prompt. SystemPrompt string `yaml:"systemPrompt"` } // EmotionParams configures the persona's emotion state machine and its per- // persona memory isolation namespace. Transitions may be empty, in which case // the emotion package installs its default transition table. type EmotionParams struct { InitialState emotion.EmotionState `yaml:"initialState"` DecayRate float64 `yaml:"decayRate"` Transitions map[emotion.EmotionState][]emotion.EmotionState `yaml:"transitions"` // MemoryNamespace is the memory isolation namespace for this persona. MemoryNamespace string `yaml:"memoryNamespace"` } // AffectionParams defines this persona's affection curve bounds and the default // step applied to an Adjust call that carries no delta. type AffectionParams struct { Initial int `yaml:"initial"` Min int `yaml:"min"` Max int `yaml:"max"` Delta int `yaml:"delta"` } // Persona is a loaded persona definition. It describes who a character is (the // identity, personality, speech style, catchphrase, background, values) and // never restricts what that character can do. // PersonaVisual carries the persona-owned visual assets (per-role icon/avatar // and optional chat background). Each persona configures its OWN visuals; the UI // renders the current persona's resources, so one role never inherits another's. type PersonaVisual struct { // Avatar is the persona's avatar image path (e.g. /images/Cyrene_Avatar/...). Avatar string `yaml:"avatar,omitempty"` // ChatBackground optionally sets the persona's chat backdrop, per orientation. ChatBackground ChatBackground `yaml:"chatBackground,omitempty"` } // ChatBackground holds per-orientation chat wallpaper paths (landscape/portrait). type ChatBackground struct { Landscape string `yaml:"landscape,omitempty"` Portrait string `yaml:"portrait,omitempty"` } // DefaultAvatar is the neutral fallback avatar shown when a persona has no // avatar configured (so a role never silently borrows another persona's view). const DefaultAvatar = "/images/User_Avatar/Default_Avatar.png" type Persona struct { ID string `yaml:"id"` Name string `yaml:"name"` CharacterRef string `yaml:"characterRef"` Identity Identity `yaml:"identity"` Personality string `yaml:"personality"` SpeechStyle string `yaml:"speechStyle"` Catchphrase string `yaml:"catchphrase"` Background string `yaml:"background"` Values []string `yaml:"values"` Emotion EmotionParams `yaml:"emotion"` Affection AffectionParams `yaml:"affection"` Visual PersonaVisual `yaml:"visual,omitempty"` // TriggerWords are the role-name words that, when a public-platform group // message mentions one, route the message into the participation PROBING // stage even without a direct @ (design/18 §2 触发②). Each persona defines its // own reminder words; they never imply identity (that stays structural). TriggerWords []string `yaml:"triggerWords,omitempty"` } // EmotionConfig converts this persona's own fields into the params consumed by // the emotion machine constructor. func (p *Persona) EmotionConfig() emotion.Params { return emotion.Params{ InitialState: p.Emotion.InitialState, DecayRate: p.Emotion.DecayRate, Transitions: p.Emotion.Transitions, } } // MemoryNamespace returns the effective memory isolation namespace for this // persona — the configured one, or a derived default of "persona." when none // is set. The derived default guarantees per-persona isolation. func (p *Persona) MemoryNamespace() string { if p.Emotion.MemoryNamespace != "" { return p.Emotion.MemoryNamespace } return defaultMemoryNS + "." + p.ID } // NewMachine builds the persona's emotion state machine from its config. func (p *Persona) NewMachine() (*emotion.Machine, error) { return emotion.NewMachine(p.EmotionConfig()) } // NewAffection builds the persona's affection tracker from its curve config, // applying default bounds when no curve was defined. func (p *Persona) NewAffection() (*emotion.Affection, error) { params := p.Affection if params.Min == 0 && params.Max == 0 && params.Initial == 0 && params.Delta == 0 { params = AffectionParams{ Initial: defaultAffectionInit, Min: defaultAffectionMin, Max: defaultAffectionMax, Delta: defaultAffectionDelta, } } return emotion.NewAffection(params.Initial, params.Min, params.Max) }